walk_join
def walk_join(
root, name
):SaveReturn and save_iter Variantsdoc(fastcore.xtras) shows an overview instead of listing every symbol.
IO utilities include:
maybe_open: accept a path or an open file, closing only files it opens.run: run a command and return stdout; raise IOError on failure.atomic_save: write a temporary file, then rename it over the destination.untar_dir: extract an archive, creating a containing directory when needed.globtastic: recursively find files with include and exclude filters.def walk(
path:pathlib.Path | str, # path to start searching
symlinks:bool=True, # follow symlinks?
keep_file:<built-in function callable>=ret_true, # function that returns True for wanted files
keep_folder:<built-in function callable>=ret_true, # function that returns True for folders to enter
skip_folder:<built-in function callable>=ret_false, # function that returns True for folders to skip
func:<built-in function callable>=walk_join, # function applied to each entry; default adds `/` to folders
ret_folders:bool=False, # include folder entries inline with files?
sort:bool=True, # sort entries alphabetically within each folder?
maxdepth:int=None, # max depth to descend (1=just immediate contents; None=unlimited)
):Generator: yields files and (optionally) folders as unified, inline-sorted entries
Get exts for comma-separated or list typ; if not found in list, return list with just types. Supported: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg
def globtastic(
path:pathlib.Path | str='.', # path to start searching
recursive:bool=True, # search subfolders
maxdepth:int=None, # max depth to descend (1=just immediate contents; None=unlimited)
symlinks:bool=True, # follow symlinks?
file_glob:str=None, # Only include files matching glob
file_re:str=None, # Only include files matching regex
path_glob:str=None, # Only include files whose full paths match glob (wildcards match separators)
path_re:str=None, # Only include files whose full paths match regex
folder_re:str=None, # Only enter folders matching regex
skip_file_glob:str=None, # Skip files matching glob
skip_file_re:str=None, # Skip files matching regex
skip_folder_re:str=None, # Skip folders matching regex,
func:<built-in function callable>=walk_join, # function to apply to each matched file
ret_folders:bool=False, # return folders, not just files
sort:bool=True, # sort files by name within each folder
types:str | list=None, # list or comma-separated str of ext types from: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg
exts:str | list=None, # list or comma-separated str of exts to include
)->fastcore.foundation.L: # Paths to matched filesA more powerful glob, including regex matches, symlink handling, and skip parameters
path is the directory to search. file_glob and file_re match filenames. path_glob and path_re match each file’s full path, including the search directory. These filters do not control directory traversal. path_glob uses fnmatch: * matches directory separators and ** has no special meaning.
['./01_basics.ipynb', './03c_aio.ipynb', './04_docments.ipynb', './05a_apisurface.ipynb', './06_script.ipynb', './14_funccall.ipynb', './fastcore/apisurface.py', './fastcore/basics.py', './fastcore/dispatch.py', './fastcore/docments.py', './fastcore/docscrape.py', './fastcore/funccall.py', './fastcore/script.py']
def pglob(
path:pathlib.Path | str='.', # path to start searching
func:<built-in function callable>=Path, # function to apply to each matched file
*, recursive:bool=True, maxdepth:int=None, symlinks:bool=True, file_glob:str=None, file_re:str=None,
path_glob:str=None, path_re:str=None, folder_re:str=None, skip_file_glob:str=None, skip_file_re:str=None,
skip_folder_re:str=None, ret_folders:bool=False, sort:bool=True, types:str | list=None, exts:str | list=None
)->fastcore.foundation.L: # Paths to matched filesShortcut for globtastic(..., call=Path)
[Path('../CHANGELOG.md'), Path('../CODE_OF_CONDUCT.md'), Path('../CONTRIBUTING.md'), Path('../README.md')]
Context manager: open f if it is a path (and close on exit)
This is useful for functions where you want to accept a path or file. maybe_open will not close your file handle if you pass one in.
For example, we can use this to reimplement imghdr.what from the Python standard library, which is written in Python 3.9 as:
Here’s an example of the use of this function:
With maybe_open, Self, and L.map_first, we can rewrite this in a much more concise and (in our opinion) clear way:
…and we can check that it still works:
…along with the version passing a file handle:
…along with the h parameter version:
Creates and returns a directory defined by path, optionally removing previous existing directory if overwrite is True
with tempfile.TemporaryDirectory() as d:
path = Path(os.path.join(d, 'new_dir'))
new_dir = mkdir(path)
assert new_dir.exists()
test_eq(new_dir, path)
# test overwrite
with open(new_dir/'test.txt', 'w') as f: f.writelines('test')
test_eq(len(list(walk(new_dir))), 1) # assert file is present
new_dir = mkdir(new_dir, overwrite=True)
test_eq(len(list(walk(new_dir))), 0) # assert file was deletedGet the MIME type for bytes data, covering common PDF, audio, video, and image types
Generator of >=0 decoded json dicts, possibly with non-json ignored text at start and end
untar file into dest, creating a directory if the root contains more than one item; recursively chown if uid/gid set
If the contents of fname contain just one file or directory, it is placed directly in dest:
With uid/gid, ownership is set on every extracted entry, including symlinks themselves (not their targets), so a dangling link inside the archive does not abort the extraction:
if hasattr(os, 'chown'): # not on Windows
with tempfile.TemporaryDirectory() as d:
src = Path(d)/'links'
src.mkdir()
(src/'dangling').symlink_to('missing-target')
shutil.make_archive(str(Path(d)/'a'), 'gztar', root_dir=d, base_dir='links')
out = untar_dir(Path(d)/'a.tar.gz', Path(d)/'out', uid=os.getuid(), gid=os.getgid())
assert (out/'dangling').is_symlink()If rename then the directory created is named based on the archive, without extension:
If the contents of fname contain multiple files and directories, a new folder in dest is created with the same name as fname (but without extension):
test_eq(repo_details('https://github.com/fastai/fastai.git'), ['fastai', 'fastai'])
test_eq(repo_details('[email protected]:fastai/nbdev.git\n'), ['fastai', 'nbdev'])Run SSH command with given arguments
Transfer multiple files with rename using persistent SSH connection
Pass cmd (splitting with shlex if string) to subprocess.run; return stdout; raise IOError if fails
You can pass a string (which will be split based on standard shell rules), a list, or pass args directly:
'pip 26.2.1 from /Users/jhoward/aai-ws/.venv/lib/python3.13/site-packages/pip (python 3.13)'
Some commands fail in non-error situations, like grep. Use ignore_ex in those cases, which will return a tuple of stdout and returncode:
run automatically decodes returned bytes to a str. Use as_bytes to skip that:
Open a file, with optional compression if gz or bz2 suffix
for suf in '.pkl','.bz2','.gz':
# delete=False is added for Windows
# https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
with tempfile.NamedTemporaryFile(suffix=suf, delete=False) as f:
fn = Path(f.name)
save_pickle(fn, 't')
t = load_pickle(fn)
f.close()
test_eq(t,'t')Parse a shell-style environment string or file
testf = """# comment
# another comment
export FOO="bar#baz"
BAR=thing # comment "ok"
baz='thong'
QUX=quux
export ZAP = "zip" # more comments
FOOBAR = 42 # trailing space and comment"""
exp = dict(FOO='bar#baz', BAR='thing', baz='thong', QUX='quux', ZAP='zip', FOOBAR='42')
test_eq(parse_env(testf), exp)Expand all wildcard imports in the given code string.
inp = """from math import *
from os import *
from random import *
def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)"""
exp = """from math import pi, sin
from os import path
from random import randint
def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)"""
test_eq(expand_wildcards(inp), exp)
inp = """from itertools import *
def func(): pass"""
test_eq(expand_wildcards(inp), inp)
inp = """def outer():
from math import *
def inner():
from os import *
return sin(pi) + path.join('a', 'b')"""
exp = """def outer():
from math import pi, sin
def inner():
from os import path
return sin(pi) + path.join('a', 'b')"""
test_eq(expand_wildcards(inp), exp)Create directory like Path.mkdir but optionally set uid/gid on newly created dirs
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir)
p1 = base/'test1'
p1.mkdir_perms()
assert p1.exists() and p1.is_dir()
p2 = base/'a'/'b'/'c'
p2.mkdir_perms(parents=True)
assert p2.exists() and (base/'a').exists()
p1.mkdir_perms(exist_ok=True)
assert p1.exists()
with expect_fail(FileExistsError): p1.mkdir_perms()
with expect_fail(FileNotFoundError): (base/'missing'/'child').mkdir_perms()Context manager for writing a file atomically via a temp file that is renamed on close
atomic_save writes a temporary file in the destination directory, then renames it on success. Readers never see a partial write. If writing fails, the original file is unchanged:
We can get a module spec from a module name:
ModuleSpec(name='fastcore.basics', loader=<_frozen_importlib_external.SourceFileLoader object>, origin='/Users/jhoward/aai-ws/fastcore/fastcore/basics.py')
…then we can load it, using the origin path:
Import dotted name without running any __init__.py
Config reads and writes an ini file with one DEFAULT section. Access keys as attributes, items, or with .get(key, default). Use create= to supply initial contents for a missing file.
types= converts values on read. Path values resolve relative to the config file’s directory. Config.find(name) searches a directory and its parents for the file.
Write settings dict to a new config file, or overwrite the existing one.
Config files are saved and read using Python’s configparser.ConfigParser, inside the DEFAULT section.
{'user': 'fastai',
'lib_name': 'fastcore',
'some_path': 'test',
'some_bool': 'True',
'some_num': '3'}
Search cfg_path and its parents to find cfg_name
Path('/Users/jhoward/aai-ws/fastcore')
Reading and writing ConfigParser ini files
Config is a convenient wrapper around ConfigParser ini files with a single section (DEFAULT).
Instantiate a Config from an ini file at cfg_path/cfg_name:
{'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'}
You can create a new file if one doesn’t exist by providing a create dict:
{'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'}
If you additionally pass save=False, the Config will contain the items from create without writing a new file:
Config passes extra keyword arguments to ConfigParser. Inline comments are disabled by default. Set inline_comment_prefixes to your comment markers, as in this # example:
# Create a complete example config file with comments
cfg_str = """\
[DEFAULT]
user = fastai # inline comment
# Library configuration
lib_name = fastcore
# Paths
some_path = test
# Feature flags
some_bool = True
# Numeric settings
some_num = # missing value
"""
with open('../tmp.ini', 'w') as f:
f.write(cfg_str)Keys can be accessed as attributes, items, or with get and an optional default:
Extra files can be read before cfg_path/cfg_name using extra_files, in the order they appear:
Pass a {key: type} mapping in types to convert values when reading them. Keys without a type return strings. For Path values, relative paths resolve from the config file’s directory. bool values use str2bool.
Search cfg_path and its parents to find cfg_name
You can use Config.find to search a path and its parents for a config file, starting in the current path if no path is specified:
dict2obj converts nested dicts to AttrDict and lists to L, allowing attribute access such as d.b.c. obj2dict reverses the conversion:
Create a collection of name/value pairs.
Example enumeration:
class Color(Enum): … RED = 1 … BLUE = 2 … GREEN = 3
Access them by:
attribute access:
Color.RED <Color.RED: 1>
value lookup:
Color(1) <Color.RED: 1>
name lookup:
Color[‘RED’] <Color.RED: 1>
Enumerations can be iterated over, and know how many members they have:
len(Color) 3
list(Color) [<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]
Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.
Convert (possibly nested) dicts (or lists of dicts) to AttrDict
This is a convenience to give you “dotted” access to (possibly nested) dictionaries, e.g:
kwargs can also be used:
It can also be used on lists of dicts.
None values should be preserved:
Convert (possibly nested) AttrDicts (or lists of AttrDicts) to dict
obj2dict can be used to reverse what is done by dict2obj:
Take contiguous lines matching pat from either end of s
take_lines selects contiguous lines matching pat from the start, end, or both. By default it returns the selected lines; drop=True returns everything else. Original line endings are preserved.
By default, matching lines are taken only from the start.
'x = 1\n'
These methods extend Pathlib.Path:
ls() lists directory contents as an L, optionally filtered by MIME prefix or extension.read_json, write_json, and read_jsonl handle JSON files.readlines reads lines; mk_write creates parent directories before writing.delete, relpath, and normpath provide file deletion and path conversions.Set Path.BASE_PATH to display paths relative to that directory.
Same as read_text followed by loads
Parse newline-delimited JSON, returning one object per line
JSON Lines uses \n to separate records. splitlines() also splits at U+0085, U+2028, and U+2029, which can occur inside valid JSON strings:
read_jsonl splits records only at \n:
[{'id': 1, 'msg': 'durable execution,\u2028backend development'},
{'id': 2, 'msg': 'plain'}]
Parse errors include the file and line number:
Make all parent dirs of self, and write data
Same as dumpsfollowed by mk_write
Same as os.path.relpath, but returns a Path, and resolves symlinks
Contents of path as a list
We add an ls() method to pathlib.Path which is simply defined as list(Path.iterdir()), mainly for convenience in REPL environments such as notebooks.
Path('llms.txt')
You can also pass an optional file_type MIME prefix and/or a list of file extensions.
(Path('../fastcore/shutil.py'), Path('000_tour.ipynb'))
normpath normalizes a path by collapsing redundant separators and up-level references (e.g., ..).
Set Path.BASE_PATH to display paths within that directory relative to it. Paths outside the directory keep their existing representation:
Reindexes collection coll with indices idxs and optional LRU cache of size cache
ReindexCollection changes the order in which you read a collection without rearranging its contents. fastai uses it to prepare data for language models.
Pass the index order in idxs, or change it later with reindex. Use descending indices to read the list in reverse order:
['e', 'd', 'c', 'b', 'a']
Alternatively, you can use the reindex method:
['e', 'd', 'c', 'b', 'a']
You can optionally specify a LRU cache, which uses functools.lru_cache upon instantiation:
CacheInfo(hits=1, misses=1, maxsize=2, currsize=1)
You can optionally clear the LRU cache by calling the cache_clear method:
CacheInfo(hits=0, misses=0, maxsize=2, currsize=0)
Without idxs, ReindexCollection starts in the collection’s original order. shuffle changes the index order:
['d', 'g', 'a', 'c', 'e', 'h', 'b', 'f']
sz = 50
t = ReindexCollection(L.range(sz), cache=2)
test_eq(list(t), range(sz))
test_eq(t[sz-1], sz-1)
test_eq(t._get.cache_info().hits, 1)
t.shuffle()
test_eq(t._get.cache_info().hits, 1)
test_ne(list(t), range(sz))
test_eq(set(t), set(range(sz)))
t.cache_clear()
test_eq(t._get.cache_info().hits, 0)
test_eq(t.count(0), 1)SaveReturn and save_iter VariantsA generator can return a final value as well as yielding values. Normal iteration discards that return value:
Wrap an iterator such that the generator function’s return value is stored in .value
SaveReturn wraps a non-async generator and captures its return value. It uses yield from, whose result is the value returned by the generator:
Values: [0, 1, 2, 3, 4]
10
In order to provide an accurate signature for save_iter, we need a version of wraps that removes leading parameters:
Like wraps, but removes the first n parameters from the signature
trim_wraps is a decorator factory that works like functools.wraps, but removes the first n parameters from the wrapped function’s signature. This is useful when creating wrapper functions that consume some parameters internally and shouldn’t expose them in the public API.
adder(x, y)
Decorator that allows a generator function to store values in the returned iterator object
save_iter passes the returned iterator to the generator as its first argument. You can store multiple attributes on that object at any point during iteration. Here o.value holds the final sum:
Call sum_range with only n. save_iter supplies o:
Values: [0, 1, 2, 3, 4]
Sum stored: 10
asave_iter works like save_iter for async generators. These cannot return a value or use yield from, so SaveReturn does not apply.
Other utilities include:
exec_eval: run a code string and return its last expression, like a notebook cell.fenced: wrap text in a Markdown fence longer than any fence character run in the body.str_diff: return a unified diff, or '' for identical text.unqid and friendly_name: generate random Python identifiers or memorable word-based names.flexicache: cache results with configurable expiration policies.Tuple of (dict, body) from frontmatter in txt; missing frontmatter returns ({}, txt), malformed YAML raises
yaml.BaseLoader extended to resolve true/True/false/False, and nothing else, to bool
The closing fence can end the text without a trailing newline.
strvals=True keeps scalar values as strings while preserving YAML’s lists and mappings. This is useful when reading amounts and dates from legal forms.
Unquoted true, True, false, and False become booleans. Keeping 'false' as a string would make a flag test treat it as true. Quoted values and yes/no/on/off remain strings:
test_eq(frontmatter('---\ntitle: "Hi there"\ntags: [a,b]\n---\nBody'), ({'title':'Hi there', 'tags':['a','b']}, 'Body'))
test_eq(frontmatter('---\nbroken: [a,'), ({}, '---\nbroken: [a,'))
test_eq(frontmatter('No frontmatter here'), ({}, 'No frontmatter here'))
test_eq(frontmatter('---\njust text no closing'), ({}, '---\njust text no closing'))
test_eq(frontmatter('---\n---\nBody'), ({}, '---\n---\nBody'))
test_fail(lambda: frontmatter('---\nbad: "unclosed\n---\nB')) # malformed raises; absent stays ({},txt)
test_eq(frontmatter('---\nt: v\n...\nBody'), ({'t':'v'}, 'Body'))
test_eq(frontmatter('---\nt: v\n---'), ({'t':'v'}, ''))
test_eq(frontmatter('---\nn: 1000\ngs:\n - {d: X, n: 1}\n---\nB', strvals=True), ({'n':'1000','gs':[{'d':'X','n':'1'}]}, 'B'))
test_eq(frontmatter('---\na: true\nb: False\nc: TRUE\nq: "true"\nnm: Norway\nn: no\n---\nB', strvals=True), (dict(a=True, b=False, c='TRUE', q='true', nm='Norway', n='no'), 'B'))Render CLI output as it appears on screen: alternate screen, \r overwrites, \b backspaces, and ANSI escapes
# Alternate screen → empty
test_eq(clean_cli_output('hello\x1b[?1049hworld'), '')
# Carriage return keeps last segment
test_eq(clean_cli_output('downloading...\rprogress 50%\rdone!'), 'done!')
# Multi-line with \r
test_eq(clean_cli_output('line1\nfoo\rbar\nline3'), 'line1\nbar\nline3')
# Trailing \r overwrites nothing: the screen still shows the text
test_eq(clean_cli_output('abc\r'), 'abc')
test_eq(clean_cli_output('download\rdone\r'), 'done')
# Backspace: a following char overwrites the previous one; trailing \b just moves the cursor
test_eq(clean_cli_output('hellp\bo world'), 'hello world')
test_eq(clean_cli_output('ab\b'), 'ab')
test_eq(clean_cli_output('\bx'), 'x')
# \b never crosses lines
test_eq(clean_cli_output('a\n\bb'), 'a\nb')
# ANSI stripping
test_eq(clean_cli_output('\x1b[31mred\x1b[0m text'), 'red text')
# OSC sequences
test_eq(clean_cli_output('\x1b]0;title\x07hello'), 'hello')
# strip_ansi=False preserves escapes
test_eq(clean_cli_output('\x1b[31mred\x1b[0m', strip=False), '\x1b[31mred\x1b[0m')
# Plain text unchanged
test_eq(clean_cli_output('just plain text\nline two'), 'just plain text\nline two')
# PTY \r\n line endings normalized to \n
test_eq(clean_cli_output('hello\r\n'), 'hello\n')
test_eq(clean_cli_output('line1\r\nline2\r\n'), 'line1\nline2\n')
# After returning from alternate screen, remaining output should be kept
test_eq(clean_cli_output('before\x1b[?1049hscreen stuff\x1b[?1049lafter'), 'after')Generate a unique id suitable for use as a Python identifier
unqid generates a random unique identifier that is safe to use as a Python variable name (starts with _, uses only alphanumeric characters and underscores). It’s based on UUID4, encoded in URL-safe base64.
If seeded=True, uses random.getrandbits which respects random.seed(), making it reproducible. Otherwise uses uuid4() which is always random.
With seeding for reproducibility:
Without seeding - always unique:
Generate a random hex string using Python’s random module.
This is the same as secrets.token_hex, but is reproducible/seedable.
Generate a random human-readable name with customizable word levels and suffix length
friendly_name generates random, human-readable names by combining adjectives, nouns, verbs, and adverbs with a random alphanumeric suffix. This is useful for creating memorable identifiers for temporary files, test data, or user-friendly resource names.
Names are hyphen-separated and follow the pattern adjective-noun-verb-adverb, randomly chosen from lists of size 102, 116, 110, and 30, respectively. The levels param selects how many of the names to include:
suffix sets the length of the random alphanumeric ending. Each suffix item is taken from the 36 options of lowercase letters plus digits.
Number of possible combos for `friendly_names
The number of combinations if all levels are included is:
The default settings give:
Evaluate code in g (defaults to globals()) and l (defaults to locals())
This is a combination of eval and exec, which behaves like ipython and Jupyter. If the last line is an expression, it is evaluated and the result is returned:
By default, the code uses the caller’s globals and locals. For instance, here f is available since it’s been added to our symbol table:
Pass a dict as the g param in order to use an arbitrary namespace:
This function helps us identify the first declared raw function of a dispatched function:
get_source_link allows you get a link to source code related to an object. For nbdev related projects such as fastcore, we can get the full link to a GitHub repo. For nbdev projects, be sure to properly set the git_url in settings.ini (derived from lib_name and branch on top of the prefix you will need to adapt) so that those links are correct.
For example, below we get the link to fastcore.test.test_eq:
'https://github.com/AnswerDotAI/fastcorefastcore/test.py#L76'
Sparkline for data, with Nones (and zero, if empty_zero) shown as empty column
without "empty_zero": ▅▂ ▁▂▁▃▇▅
with "empty_zero": ▅▂ ▁▂ ▃▇▅
You can set a maximum and minimum for the y-axis of the sparkline with the arguments mn and mx respectively:
Modifies e with a custom message attached
msg = "This is my custom message!"
with expect_fail(Exception, ''): (_ for _ in ()).throw(modify_exception(Exception(), None))
with expect_fail(Exception, msg): (_ for _ in ()).throw(modify_exception(Exception(), msg))
with expect_fail(Exception, "The first message This is my custom message!"): (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg))
with expect_fail(Exception, "This is my custom message!"): (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg, True))Round x to nearest multiple of mult
This sets the number of threads consistently for many tools, by:
nt: OPENBLAS_NUM_THREADS,NUMEXPR_NUM_THREADS,OMP_NUM_THREADS,MKL_NUM_THREADSnt threads for numpy and pytorch.Return path/file if file is a string or a Path, file otherwise
An event timer with history of store items of time span
Add events with add, and get number of events and their frequency (freq).
# Random wait function for testing
def _randwait(): yield from (sleep(random.random()/200) for _ in range(100))
c = EventTimer(store=5, span=0.03)
for o in _randwait(): c.add(1)
print(f'Num Events: {c.events}, Freq/sec: {c.freq:.01f}')
print('Most recent: ', sparkline(c.hist), *L(c.hist).map('{:.01f}'))Num Events: 9, Freq/sec: 334.2
Most recent: ▃▂▇▁▆ 263.0 253.9 283.5 243.7 273.8
A string.Formatter that doesn’t error on missing fields, and tracks missing fields and unused args
string format s, ignoring missing field errors, returning missing and extra fields
The result is a tuple of (formatted_string,missing_fields,extra_fields), e.g:
Truncate s to length maxlen, adding suffix suf if truncated; a callable suf gets the number of characters cut
A callable suf receives the number of characters removed, including those displaced by the suffix itself:
w = 'abacadabra'
test_eq(truncstr(w, 10), w)
test_eq(truncstr(w, 5), 'abac…')
test_eq(truncstr(w, 5, suf=''), 'abaca')
test_eq(truncstr(w, 11, space='_'), w+"_")
test_eq(truncstr(w, 10, space='_'), w[:-1]+'…')
test_eq(truncstr(w, 5, suf='!!'), 'aba!!')
test_eq(truncstr(w, 8, suf=lambda n: f'…[{n}]'), 'abac…[6]')
test_eq(truncstr('x'*5000, 20, suf=lambda n: f'…[{humanize(n)}]'), 'x'*15+'…[5k]')Set sizevar='_n_' to substitute the original string length for {_n_} in truncstr’s suffix. Here (11) records the length before truncation:
trunc_ctr keeps the head and tail of a long string, elides the middle, and marks the elision with its humanized size. Use it when both ends of a document matter, such as a summary at the top and the discussion at the end.
Truncate the middle of s so ~mx chars remain, marking the elision with its humanized size
'summar…[34 chars]…eplies'
Wrap a large tool result in TruncatedString to shorten its bare display without discarding text. Its repr uses trunc_ctr. Slicing, searching and print still use the complete string.
A str whose repr shows contents verbatim, middle-truncated to ~mx chars
summar…[34 chars]…eplies
Wrap text in a fence of ch, one char longer than any run of ch inside it (min 3)
When generated text goes inside a markdown code fence, the fence must be longer than any run of the fence character in the body, or the block ends early. fenced picks a safe fence: one char longer than the longest such run anywhere in the text, at least the standard three.
```python
x=1
```
Fence character runs count anywhere in the text, including mid-line. info is appended verbatim; include a leading space for forms such as ::: output. Trailing newlines are stripped. An empty body leaves one blank line between the fences.
Top-level fenced blocks in text, fence-nesting-aware: the inverse of fenced
Only top-level blocks are returned. A closing fence must be at least as long as its opener and contain no other text. Shorter fences remain in the body. Unclosed blocks are omitted.
s = "before\n\n```json {.tool}\n{\"a\": 1}\n```\n\nafter\n"
blks = fenced_blocks(s)
test_eq(len(blks), 1)
info,body,start,end = blks[0]
test_eq(info, 'json {.tool}')
test_eq(body, '{"a": 1}\n')
test_eq(s[start:end], '```json {.tool}\n{"a": 1}\n```\n')
outer = fenced(s, 'markdown') # embed the whole thing in a longer fence
test_eq(fenced_blocks(outer)[0][0], 'markdown') # only the top-level block reported
test_eq(fenced_blocks('```\nopen\n'), []) # unclosed: no block
test_eq(fenced_blocks('::::a\nx\n::::\n', ch=':')[0][0], 'a')str_diff returns a unified diff as a plain string, or '' when the texts match.
Unified diff of a and b
Convert dt from UTC to local time
2000-01-01 12:00:00 UTC is 2000-01-01 22:00:00+10:00 local time
Convert dt from local to UTC time
2000-01-01 12:00:00 local is 2000-01-01 02:00:00+00:00 UTC time
You can add a breakpoint to an existing function, e.g:
Now, when the function is called it will drop you into the debugger. Note, you must issue the s command when you begin to step into the function that is being traced.
Context manager temporarily modifying os.environ by deleting delete and replacing replace
# USER isn't in Cloud Linux Environments
env_test = 'USERNAME' if sys.platform == "win32" else 'SHELL'
oldusr = os.environ[env_test]
replace_param = {env_test: 'a'}
with modified_env('PATH', **replace_param):
test_eq(os.environ[env_test], 'a')
assert 'PATH' not in os.environ
assert 'PATH' in os.environ
test_eq(os.environ[env_test], oldusr)Wrapper for contextlib.ExitStack which enters a collection of context managers
Randomly relocate items of x up to pct of len(x) from their starting location
When we display code in a notebook, it’s nice to highlight it, so we create a function to simplify that:
@dataclass
class DC:
x: int
y: Union[float, None] = None
z: float = None
Like dataclass, but default of UNSET added to fields without defaults
Person(name='Bob', age=UNSET, city='Unknown')
Person(name='Bob', age=UNSET, city='NY')
Convert cls into a dataclass like make_nullable. Converts in place and also returns the result.
This can be used as a decorator…
Person(name='Bob', age=UNSET, city='Unknown')
…or can update the behavior of an existing class (or dataclass):
Person(name='Bob', age=UNSET, city='Unknown')
Action occurs in-place:
True
Convert o to a dict, supporting dataclasses, namedtuples, iterables, and __dict__ attrs.
Any UNSET values are not included.
Set the optional __flds__ parameter to customise the field list, and the optional __skip__ parameter to skip some names.
An object may be iterable without yielding key-value pairs; since dict can’t consume it, asdict falls back to its instance attributes.
To customise dict conversion behavior for a class, implement the _asdict method (this is used in the Python stdlib for named tuples).
The vars_pub function returns a list of public (non-underscore-prefixed) variable names from an object, excluding any names listed in the object’s optional __skip__ attribute.
Without __skip__, all pub vars are returned
Like lru_cache, but customisable with policy funcs
flexicache adds expiration policies to an LRU cache. time_policy expires results after an interval; mtime_policy expires them when a file changes.
When caching a new result, flexicache calls each policy with None. Return the state to save for that cache entry. On later lookups, flexicache passes the saved state to the policy. Return a truthy value to expire the result, or a falsy value to reuse it. After recomputing an expired result, flexicache calls each policy with None to save new state.
Set a policy’s needs_args=True to receive (state, args, kwargs) instead. This lets mtime_policy(arg=...) watch a file named in the cached call’s arguments.
A flexicache policy that expires cached items after seconds have passed
A flexicache policy that expires cached items after the watched file’s modified-time changes
3
3
3
fp = Path('flexi-test.txt')
fp.write_text('a')
@flexicache(mtime_policy(arg=0))
def slurp(p): return p.read_text()
test_eq(slurp(fp), 'a')
fp.write_text('b')
os.utime(fp, (time()+1,)*2)
test_eq(slurp(fp), 'b')
fp.write_text('c')
os.utime(fp, (time()-1,)*2)
test_eq(slurp(fp), 'c')
@flexicache(mtime_policy(arg='p'))
def slurp2(p=None): return p.read_text()
test_eq(slurp2(p=fp), 'c')
fp.unlink()Like lru_cache, but also with time-based eviction
# demonstrate that flexicache is LRU
@flexicache(maxsize=2)
def cached_func(x): return time()
time_1 = cached_func(1)
test_eq(time_1, cached_func(1))
time_2 = cached_func(2)
test_eq(time_1, cached_func(1))
test_eq(time_2, cached_func(2))
time_3 = cached_func(3) # Removes 1
test_eq(time_2, cached_func(2)) # cache remains
test_eq(time_3, cached_func(3)) # cache remains
test_ne(time_1, cached_func(1)) # NEQ, removes 2
test_ne(time_2, cached_func(2)) # NEQ, removes 3
test_eq(cached_func(1), cached_func(1))This function is a small convenience wrapper for using flexicache with time_policy.
@timed_cache(seconds=0.05, maxsize=2)
def cached_func(x): return x * 2, time()
# basic caching
result1, time1 = cached_func(2)
test_eq(result1, 4)
sleep(0.001)
result2, time2 = cached_func(2)
test_eq(result2, 4)
test_eq(time1, time2)
# caching different values
result3, _ = cached_func(3)
test_eq(result3, 6)
# maxsize
_, time4 = cached_func(4)
_, time2_new = cached_func(2)
test_close(time2, time2_new, eps=0.1)
_, time3_new = cached_func(3)
test_ne(time3_new, time())
# time expiration
sleep(0.05)
_, time4_new = cached_func(4)
test_ne(time4_new, time())