fastcore 2.2.22__tar.gz → 2.2.23__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {fastcore-2.2.22/fastcore.egg-info → fastcore-2.2.23}/PKG-INFO +2 -2
  2. {fastcore-2.2.22 → fastcore-2.2.23}/README.md +1 -1
  3. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/__init__.py +2 -2
  4. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/_modidx.py +3 -8
  5. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/apisurface.py +25 -25
  6. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/docments.py +162 -155
  7. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/nbio.py +15 -9
  8. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/xtras.py +9 -4
  9. {fastcore-2.2.22 → fastcore-2.2.23/fastcore.egg-info}/PKG-INFO +2 -2
  10. {fastcore-2.2.22 → fastcore-2.2.23}/CONTRIBUTING.md +0 -0
  11. {fastcore-2.2.22 → fastcore-2.2.23}/LICENSE +0 -0
  12. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/aio.py +0 -0
  13. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/all.py +0 -0
  14. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/ansi.py +0 -0
  15. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/basics.py +0 -0
  16. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/dispatch.py +0 -0
  17. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/docscrape.py +0 -0
  18. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/editskill.py +0 -0
  19. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/foundation.py +0 -0
  20. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/funccall.py +0 -0
  21. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/imghdr.py +0 -0
  22. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/imports.py +0 -0
  23. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/meta.py +0 -0
  24. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/nb_imports.py +0 -0
  25. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/net.py +0 -0
  26. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/parallel.py +0 -0
  27. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/py2pyi.py +0 -0
  28. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/script.py +0 -0
  29. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/shutil.py +0 -0
  30. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/style.py +0 -0
  31. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/test.py +0 -0
  32. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/tools.py +0 -0
  33. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/transform.py +0 -0
  34. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/utils.py +0 -0
  35. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/xdg.py +0 -0
  36. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore/xml.py +0 -0
  37. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore.egg-info/SOURCES.txt +0 -0
  38. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore.egg-info/dependency_links.txt +0 -0
  39. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore.egg-info/entry_points.txt +0 -0
  40. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore.egg-info/requires.txt +0 -0
  41. {fastcore-2.2.22 → fastcore-2.2.23}/fastcore.egg-info/top_level.txt +0 -0
  42. {fastcore-2.2.22 → fastcore-2.2.23}/pyproject.toml +0 -0
  43. {fastcore-2.2.22 → fastcore-2.2.23}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastcore
3
- Version: 2.2.22
3
+ Version: 2.2.23
4
4
  Summary: Python supercharged for fastai development
5
5
  Author-email: Jeremy Howard and Sylvain Gugger <infos@fast.ai>
6
6
  License: Apache-2.0
@@ -54,7 +54,7 @@ Here are some tips on using fastcore:
54
54
  - **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path).
55
55
  - **Flexible data structures**: Convert between dictionaries and attribute-access objects using [`dict2obj`](https://fastcore.fast.ai/xtras.html#dict2obj) and [`obj2dict`](https://fastcore.fast.ai/xtras.html#obj2dict) for more intuitive data handling.
56
56
  - **Functional programming paradigms**: Use tools like [`compose`](https://fastcore.fast.ai/basics.html#compose), [`maps`](https://fastcore.fast.ai/basics.html#maps), and [`filter_ex`](https://fastcore.fast.ai/basics.html#filter_ex) to write more functional-style Python code.
57
- - **Documentation**: Use [`docments`](https://fastcore.fast.ai/docments.html#docments) where possible to document parameters of functions and methods.
57
+ - **Documentation**: Document parameters and return values with [`docments`](https://fastcore.fast.ai/docments.html#docments), using source comments or `Annotated` metadata for generated APIs. [`MarkdownRenderer`](https://fastcore.fast.ai/docments.html#markdownrenderer) shows the effective signature and retains usage sections such as Notes, Raises, and Examples.
58
58
  - **Time-aware caching**: Apply the [`timed_cache`](https://fastcore.fast.ai/xtras.html#timed_cache) decorator to add time-based expiration to the standard `lru_cache` functionality.
59
59
  - **Simplified CLI creation**: Use `fastcore.script` to easily transform Python functions into command-line interfaces.
60
60
 
@@ -23,7 +23,7 @@ Here are some tips on using fastcore:
23
23
  - **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path).
24
24
  - **Flexible data structures**: Convert between dictionaries and attribute-access objects using [`dict2obj`](https://fastcore.fast.ai/xtras.html#dict2obj) and [`obj2dict`](https://fastcore.fast.ai/xtras.html#obj2dict) for more intuitive data handling.
25
25
  - **Functional programming paradigms**: Use tools like [`compose`](https://fastcore.fast.ai/basics.html#compose), [`maps`](https://fastcore.fast.ai/basics.html#maps), and [`filter_ex`](https://fastcore.fast.ai/basics.html#filter_ex) to write more functional-style Python code.
26
- - **Documentation**: Use [`docments`](https://fastcore.fast.ai/docments.html#docments) where possible to document parameters of functions and methods.
26
+ - **Documentation**: Document parameters and return values with [`docments`](https://fastcore.fast.ai/docments.html#docments), using source comments or `Annotated` metadata for generated APIs. [`MarkdownRenderer`](https://fastcore.fast.ai/docments.html#markdownrenderer) shows the effective signature and retains usage sections such as Notes, Raises, and Examples.
27
27
  - **Time-aware caching**: Apply the [`timed_cache`](https://fastcore.fast.ai/xtras.html#timed_cache) decorator to add time-based expiration to the standard `lru_cache` functionality.
28
28
  - **Simplified CLI creation**: Use `fastcore.script` to easily transform Python functions into command-line interfaces.
29
29
 
@@ -14,7 +14,7 @@ Here are some tips on using fastcore:
14
14
  - **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to `pathlib.Path`.
15
15
  - **Flexible data structures**: Convert between dictionaries and attribute-access objects using `dict2obj` and `obj2dict` for more intuitive data handling.
16
16
  - **Functional programming paradigms**: Use tools like `compose`, `maps`, and `filter_ex` to write more functional-style Python code.
17
- - **Documentation**: Use `docments` where possible to document parameters of functions and methods.
17
+ - **Documentation**: Document parameters and return values with `docments`, using source comments or `Annotated` metadata for generated APIs. `MarkdownRenderer` shows the effective signature and retains usage sections such as Notes, Raises, and Examples.
18
18
  - **Time-aware caching**: Apply the `timed_cache` decorator to add time-based expiration to the standard `lru_cache` functionality.
19
19
  - **Simplified CLI creation**: Use `fastcore.script` to easily transform Python functions into command-line interfaces.
20
20
 
@@ -55,4 +55,4 @@ Modules:
55
55
  - `fastcore.xml`: Concise generation of XML.
56
56
  - `fastcore.xtras`: Utility functions used in the fastai library"""
57
57
 
58
- __version__ = "2.2.22"
58
+ __version__ = "2.2.23"
@@ -315,11 +315,8 @@ d = { 'settings': { 'branch': 'main',
315
315
  'fastcore.docments.DocmentTbl.__eq__': ('docments.html#docmenttbl.__eq__', 'fastcore/docments.py'),
316
316
  'fastcore.docments.DocmentTbl.__init__': ('docments.html#docmenttbl.__init__', 'fastcore/docments.py'),
317
317
  'fastcore.docments.DocmentTbl._columns': ('docments.html#docmenttbl._columns', 'fastcore/docments.py'),
318
- 'fastcore.docments.DocmentTbl._hdr_list': ('docments.html#docmenttbl._hdr_list', 'fastcore/docments.py'),
319
318
  'fastcore.docments.DocmentTbl._repr_markdown_': ( 'docments.html#docmenttbl._repr_markdown_',
320
319
  'fastcore/docments.py'),
321
- 'fastcore.docments.DocmentTbl._row': ('docments.html#docmenttbl._row', 'fastcore/docments.py'),
322
- 'fastcore.docments.DocmentTbl._row_list': ('docments.html#docmenttbl._row_list', 'fastcore/docments.py'),
323
320
  'fastcore.docments.DocmentTbl.has_docment': ( 'docments.html#docmenttbl.has_docment',
324
321
  'fastcore/docments.py'),
325
322
  'fastcore.docments.DocmentTbl.has_return': ( 'docments.html#docmenttbl.has_return',
@@ -361,12 +358,11 @@ d = { 'settings': { 'branch': 'main',
361
358
  'fastcore.docments._DocstringExtractor.visit_Module': ( 'docments.html#_docstringextractor.visit_module',
362
359
  'fastcore/docments.py'),
363
360
  'fastcore.docments._bold': ('docments.html#_bold', 'fastcore/docments.py'),
361
+ 'fastcore.docments._callable_instance': ('docments.html#_callable_instance', 'fastcore/docments.py'),
364
362
  'fastcore.docments._clean_comment': ('docments.html#_clean_comment', 'fastcore/docments.py'),
365
363
  'fastcore.docments._clean_text_sig': ('docments.html#_clean_text_sig', 'fastcore/docments.py'),
366
364
  'fastcore.docments._docstring': ('docments.html#_docstring', 'fastcore/docments.py'),
367
365
  'fastcore.docments._escape_markdown': ('docments.html#_escape_markdown', 'fastcore/docments.py'),
368
- 'fastcore.docments._f_name': ('docments.html#_f_name', 'fastcore/docments.py'),
369
- 'fastcore.docments._fmt_anno': ('docments.html#_fmt_anno', 'fastcore/docments.py'),
370
366
  'fastcore.docments._fmt_default': ('docments.html#_fmt_default', 'fastcore/docments.py'),
371
367
  'fastcore.docments._fmt_sig': ('docments.html#_fmt_sig', 'fastcore/docments.py'),
372
368
  'fastcore.docments._fullname': ('docments.html#_fullname', 'fastcore/docments.py'),
@@ -377,13 +373,11 @@ d = { 'settings': { 'branch': 'main',
377
373
  'fastcore.docments._ital_first': ('docments.html#_ital_first', 'fastcore/docments.py'),
378
374
  'fastcore.docments._list2row': ('docments.html#_list2row', 'fastcore/docments.py'),
379
375
  'fastcore.docments._maybe_nm': ('docments.html#_maybe_nm', 'fastcore/docments.py'),
380
- 'fastcore.docments._merge_doc': ('docments.html#_merge_doc', 'fastcore/docments.py'),
381
376
  'fastcore.docments._merge_docs': ('docments.html#_merge_docs', 'fastcore/docments.py'),
382
- 'fastcore.docments._non_empty_keys': ('docments.html#_non_empty_keys', 'fastcore/docments.py'),
383
377
  'fastcore.docments._param_locs': ('docments.html#_param_locs', 'fastcore/docments.py'),
384
378
  'fastcore.docments._parses': ('docments.html#_parses', 'fastcore/docments.py'),
385
- 'fastcore.docments._show_param': ('docments.html#_show_param', 'fastcore/docments.py'),
386
379
  'fastcore.docments._tokens': ('docments.html#_tokens', 'fastcore/docments.py'),
380
+ 'fastcore.docments._type_str': ('docments.html#_type_str', 'fastcore/docments.py'),
387
381
  'fastcore.docments._unwrap_sym': ('docments.html#_unwrap_sym', 'fastcore/docments.py'),
388
382
  'fastcore.docments.ann_parts': ('docments.html#ann_parts', 'fastcore/docments.py'),
389
383
  'fastcore.docments.can_render': ('docments.html#can_render', 'fastcore/docments.py'),
@@ -650,6 +644,7 @@ d = { 'settings': { 'branch': 'main',
650
644
  'fastcore.nbio.new_nb': ('nbio.html#new_nb', 'fastcore/nbio.py'),
651
645
  'fastcore.nbio.preferred_msg_out': ('nbio.html#preferred_msg_out', 'fastcore/nbio.py'),
652
646
  'fastcore.nbio.preferred_out': ('nbio.html#preferred_out', 'fastcore/nbio.py'),
647
+ 'fastcore.nbio.prev_line': ('nbio.html#prev_line', 'fastcore/nbio.py'),
653
648
  'fastcore.nbio.read_nb': ('nbio.html#read_nb', 'fastcore/nbio.py'),
654
649
  'fastcore.nbio.render_output': ('nbio.html#render_output', 'fastcore/nbio.py'),
655
650
  'fastcore.nbio.render_outputs': ('nbio.html#render_outputs', 'fastcore/nbio.py'),
@@ -12,12 +12,13 @@ Docs: https://fastcore.fast.ai/apisurface.html.md"""
12
12
  __all__ = ['snake', 'sanitize_param_name', 'sanitized_params', 'mk_sig', 'mk_doc', 'OpGroup', 'mk_groups', 'full_docs']
13
13
 
14
14
  # %% ../nbs/05a_apisurface.ipynb #b2797a42
15
- import keyword
15
+ import keyword,inspect
16
16
  from inspect import Parameter, Signature
17
17
  from urllib.parse import urlparse, urljoin
18
18
  from .imports import *
19
19
  from .basics import *
20
20
  from .xtras import UNSET
21
+ from .docments import ann_parts
21
22
 
22
23
  # %% ../nbs/05a_apisurface.ipynb #475598fa
23
24
  _pat_non_alnum = re.compile(r"[^a-zA-Z0-9]+")
@@ -32,13 +33,6 @@ def snake(s: str):
32
33
  # %% ../nbs/05a_apisurface.ipynb #3e19a6d0
33
34
  def sanitize_param_name(p): return snake(re.sub(r'\W', '_', p).strip('_'))
34
35
 
35
- # %% ../nbs/05a_apisurface.ipynb #f44a2c97
36
- def _mk_param(name, required, anno=None, default=None):
37
- "Create a function signature parameter."
38
- anno = Parameter.empty if anno is None else anno
39
- if default is None: default = Parameter.empty if required else UNSET
40
- return Parameter(name, kind=Parameter.POSITIONAL_OR_KEYWORD, default=default, annotation=anno)
41
-
42
36
  # %% ../nbs/05a_apisurface.ipynb #c10b6dcc
43
37
  def sanitized_params(ps):
44
38
  "Mapping from spec param names `ps` to valid Python identifiers; exact names win collisions."
@@ -55,21 +49,28 @@ def sanitized_params(ps):
55
49
  res[p] = name
56
50
  return res
57
51
 
58
- # %% ../nbs/05a_apisurface.ipynb #32fca522
52
+ # %% ../nbs/05a_apisurface.ipynb #f44a2c97
53
+ def _mk_param(name, required, anno=None, default=Parameter.empty):
54
+ "Create a function signature parameter."
55
+ anno = Parameter.empty if anno is None else anno
56
+ if default is Parameter.empty: default = Parameter.empty if required else UNSET
57
+ return Parameter(name, kind=Parameter.POSITIONAL_OR_KEYWORD, default=default, annotation=anno)
58
+
59
59
  def _sort_key(o):
60
60
  if o.default is Parameter.empty: return 0
61
61
  if o.default is UNSET: return 1
62
62
  return 2
63
63
 
64
- # %% ../nbs/05a_apisurface.ipynb #4f63ac5e
65
64
  def mk_sig(op, sparams=None, defaults=None):
66
- "An `inspect.Signature` for operation record `op`; `defaults` values make their params optional."
65
+ "An operation signature with parameter descriptions in `Annotated`; `defaults` makes those params optional"
67
66
  if sparams is None: sparams = sanitized_params(op.params)
68
67
  defaults = defaults or {}
69
68
  params = []
70
69
  for pname, sname in sparams.items():
71
- default = defaults.get(pname, op.param_defaults.get(pname))
72
- params.append(_mk_param(sname, pname in op.required_params, op.param_types.get(pname), default))
70
+ default = defaults.get(pname, op.param_defaults.get(pname, Parameter.empty))
71
+ anno = op.param_types.get(pname)
72
+ if doc := op.param_docs.get(pname): anno = typing.Annotated[anno or typing.Any, doc]
73
+ params.append(_mk_param(sname, pname in op.required_params, anno, default))
73
74
  return Signature(sorted(params, key=_sort_key))
74
75
 
75
76
  # %% ../nbs/05a_apisurface.ipynb #940536d9
@@ -81,14 +82,6 @@ def _op_summary(op):
81
82
  base = f"{p.scheme}://{p.netloc}"
82
83
  return re.sub(r"\]\((/[^)]+)\)", lambda m: f"]({urljoin(base, m[1].strip())})", s)
83
84
 
84
- # %% ../nbs/05a_apisurface.ipynb #08067b8a
85
- def _op_line(op, sig):
86
- head = f"{'.'.join(snake(g) for g in listify(op.group))}.{op.name}"
87
- if op.docs_url: head = f"[{head}]({op.docs_url})"
88
- s = f"({', '.join(sig.parameters)})"
89
- summ = _op_summary(op)
90
- return f"{head}{s}: *{summ}*"
91
-
92
85
  # %% ../nbs/05a_apisurface.ipynb #ed62e194
93
86
  def mk_doc(op, sig, sparams):
94
87
  "Render operation docstring with summary, docs URL, and parameter hints."
@@ -102,11 +95,19 @@ def mk_doc(op, sig, sparams):
102
95
  for nm,p in sig.parameters.items():
103
96
  orig = rsparams.get(nm, nm)
104
97
  r = f"default: {p.default!r}" if p.default not in (Parameter.empty, UNSET) else "required" if orig in req else "optional"
105
- ann = '' if p.annotation is Parameter.empty else p.annotation.__name__
98
+ ann = '' if p.annotation is Parameter.empty else inspect.formatannotation(ann_parts(p.annotation)[0]).removeprefix('typing.')
106
99
  desc = (op.param_docs or {}).get(orig, "")
107
100
  lines.append(f"- {nm} ({ann}, {r}){': ' + desc if desc else ''}")
108
101
  return "\n".join(lines)
109
102
 
103
+ # %% ../nbs/05a_apisurface.ipynb #08067b8a
104
+ def _op_line(op, sig):
105
+ head = f"{'.'.join(snake(g) for g in listify(op.group))}.{op.name}"
106
+ if op.docs_url: head = f"[{head}]({op.docs_url})"
107
+ s = f"({', '.join(sig.parameters)})"
108
+ summ = _op_summary(op)
109
+ return f"{head}{s}: *{summ}*"
110
+
110
111
  # %% ../nbs/05a_apisurface.ipynb #e4d35409
111
112
  class OpGroup:
112
113
  "Namespace for grouped operations: each op is an attribute, and the repr lists them all"
@@ -123,10 +124,9 @@ class OpGroup:
123
124
  def __dir__(self): return object.__dir__(self)
124
125
  def __allow__(self): return self.ops + list(self.subgroups.values())
125
126
 
126
- def _repr_markdown_(self): return self.__doc__
127
+ def _repr_markdown_(self): return self.__doc__ + '\n\nOverview only. Read `doc(group.operation)` for parameter details and `doc(group.subgroup)` to descend.'
127
128
  __repr__ = _repr_markdown_
128
129
 
129
-
130
130
  # %% ../nbs/05a_apisurface.ipynb #62b12f6b
131
131
  def mk_groups(ops):
132
132
  "Nested tree of `OpGroup`s from ops, following each op's `group` path."
@@ -152,6 +152,6 @@ def _group_docs(name, g, lvl=2):
152
152
  return "\n\n".join(res)
153
153
 
154
154
  def full_docs(groups):
155
- "Complete markdown API reference for a `mk_groups` tree: every group and operation."
155
+ "Markdown overview of every group and operation in a `mk_groups` tree."
156
156
  return "\n\n".join(_group_docs(nm, g) for nm,g in sorted(groups.items()))
157
157
 
@@ -29,23 +29,27 @@ from tokenize import tokenize,COMMENT
29
29
  from ast import parse,FunctionDef,AsyncFunctionDef,AnnAssign
30
30
  from io import BytesIO
31
31
  from textwrap import dedent
32
- from types import SimpleNamespace
33
- from inspect import getsource,isfunction,ismethod,isclass,signature,Parameter,Signature
34
- from dataclasses import dataclass, is_dataclass
32
+ from inspect import getsource,isfunction,ismethod,isclass,signature,Parameter
33
+ from dataclasses import is_dataclass
35
34
  from .utils import *
36
- from .meta import delegates
37
35
  from . import docscrape
38
- from textwrap import fill
39
36
  from inspect import isclass,getdoc,signature
40
37
 
41
38
  # %% ../nbs/04_docments.ipynb #53e21187
39
+ def _callable_instance(sym): return callable(sym) and not (inspect.isroutine(sym) or isclass(sym))
40
+
42
41
  def docstring(sym):
43
- "Get docstring for `sym` for functions ad classes"
42
+ "Get documentation from an object, its constructor, or its callable implementation"
44
43
  if isinstance(sym, str): return sym
44
+ if isinstance(sym, partial): return docstring(sym.func)
45
+ if _callable_instance(sym):
46
+ own = inspect.getattr_static(sym, '__doc__', None)
47
+ if own is not getattr(type(sym), '__doc__', None): return getdoc(sym) or ''
48
+ return getdoc(sym.__call__) or getdoc(sym) or ''
45
49
  res = getdoc(sym)
46
- if not res and isclass(sym): res = getdoc(sym.__init__)
47
- if not res and callable(sym) and not isclass(sym): res = getdoc(sym.__call__)
48
- return res or ""
50
+ if not getattr(sym, '__doc__', None) and res == object.__init__.__doc__: res = None
51
+ if not res and isclass(sym) and sym.__init__ is not object.__init__: res = docstring(sym.__init__)
52
+ return res or ''
49
53
 
50
54
  # %% ../nbs/04_docments.ipynb #1e0cf854
51
55
  def parse_docstring(sym):
@@ -57,17 +61,45 @@ def isdataclass(s):
57
61
  "Check if `s` is a dataclass but not a dataclass' instance"
58
62
  return is_dataclass(s) and isclass(s)
59
63
 
60
- # %% ../nbs/04_docments.ipynb #6549c4b2
61
64
  def get_dataclass_source(s):
62
65
  "Get source code for dataclass `s`"
63
66
  return getsource(s) if not getattr(s, "__module__") == '__main__' else ""
64
67
 
65
- # %% ../nbs/04_docments.ipynb #cac59989
66
68
  def get_source(s):
67
- "Get source code for string, function object or dataclass `s`"
69
+ "Get source for a function, callable implementation, dataclass, or source string"
68
70
  if isinstance(s,str): return s
71
+ s = inspect.unwrap(s)
72
+ if isinstance(s, partial): s = s.func
73
+ if _callable_instance(s): s = s.__call__
69
74
  return getsource(s) if isfunction(s) or ismethod(s) else get_dataclass_source(s) if isdataclass(s) else None
70
75
 
76
+ # %% ../nbs/04_docments.ipynb #40e3f274
77
+ def _get_property_name(p):
78
+ "Get the name of property `p`"
79
+ if hasattr(p, 'fget'): return p.fget.func.__qualname__ if hasattr(p.fget, 'func') else p.fget.__qualname__
80
+ else: return next(iter(re.findall(r'\'(.*)\'', str(p)))).split('.')[-1]
81
+
82
+ def get_name(obj):
83
+ "Get the name of `obj`"
84
+ if isinstance(obj, partial):
85
+ nm = get_name(obj.func)
86
+ args = [repr(a) for a in obj.args] + [f'{k}={repr(v)}' for k,v in obj.keywords.items()]
87
+ return f"{nm}[partial: {', '.join(args)}]"
88
+ if hasattr(obj, '__name__'): return obj.__name__
89
+ elif getattr(obj, '_name', False): return obj._name
90
+ elif hasattr(obj,'__origin__'): return str(obj.__origin__).split('.')[-1]
91
+ elif type(obj)==property: return _get_property_name(obj)
92
+ elif callable(obj): return type(obj).__name__
93
+ else: return str(obj).split('.')[-1]
94
+
95
+
96
+ # %% ../nbs/04_docments.ipynb #5e273f22
97
+ def qual_name(obj):
98
+ "Get the qualified name of `obj`"
99
+ if hasattr(obj,'__qualname__'): return obj.__qualname__
100
+ if ismethod(obj): return f"{get_name(obj.__self__)}.{get_name(obj)}"
101
+ return get_name(obj)
102
+
71
103
  # %% ../nbs/04_docments.ipynb #91c0d15f
72
104
  def _parses(s):
73
105
  "Parse source of function, method, or dataclass `s`"
@@ -87,13 +119,14 @@ def _clean_comment(s):
87
119
  res = _clean_re.findall(s)
88
120
  return res[0] if res else None
89
121
 
122
+ # %% ../nbs/04_docments.ipynb #448097bb
90
123
  def _param_locs(s, returns=True, args_kwargs=False):
91
124
  "`dict` of parameter line numbers to names"
92
125
  body = _parses(s).body
93
126
  if len(body)==1:
94
127
  defn = body[0]
95
128
  if isinstance(defn, (FunctionDef, AsyncFunctionDef)):
96
- res = {arg.lineno:arg.arg for arg in defn.args.args}
129
+ res = {arg.lineno:arg.arg for arg in [*defn.args.posonlyargs, *defn.args.args]}
97
130
  # Add *args if present
98
131
  if defn.args.vararg: res[defn.args.vararg.lineno] = defn.args.vararg.arg
99
132
  # Add keyword-only args
@@ -127,49 +160,6 @@ def _get_full(p, docs, eval_str=False):
127
160
  elif eval_str: anno = None
128
161
  return AttrDict(docment=docs.get(p.name), anno=anno, default=p.default, kind=p.kind)
129
162
 
130
- # %% ../nbs/04_docments.ipynb #1b4d817c
131
- def _merge_doc(dm, npdoc):
132
- if not npdoc: return dm
133
- if not isinstance(dm, dict): return dm or '\n'.join(npdoc.desc)
134
- # if not dm.anno or dm.anno==empty: dm.anno = npdoc.type
135
- if not dm.docment: dm.docment = '\n'.join(npdoc.desc)
136
- return dm
137
-
138
- def _merge_docs(dms, npdocs):
139
- npparams = npdocs['Parameters']
140
- params = {nm:_merge_doc(dm,npparams.get(nm,None)) for nm,dm in dms.items()}
141
- if 'return' in dms: params['return'] = _merge_doc(dms['return'], npdocs['Returns'])
142
- return params
143
-
144
- # %% ../nbs/04_docments.ipynb #40e3f274
145
- def _get_property_name(p):
146
- "Get the name of property `p`"
147
- if hasattr(p, 'fget'):
148
- return p.fget.func.__qualname__ if hasattr(p.fget, 'func') else p.fget.__qualname__
149
- else: return next(iter(re.findall(r'\'(.*)\'', str(p)))).split('.')[-1]
150
-
151
- # %% ../nbs/04_docments.ipynb #da0465e3
152
- def get_name(obj):
153
- "Get the name of `obj`"
154
- if isinstance(obj, partial):
155
- nm = get_name(obj.func)
156
- args = [repr(a) for a in obj.args] + [f'{k}={repr(v)}' for k,v in obj.keywords.items()]
157
- return f"{nm}[partial: {', '.join(args)}]"
158
- if hasattr(obj, '__name__'): return obj.__name__
159
- elif getattr(obj, '_name', False): return obj._name
160
- elif hasattr(obj,'__origin__'): return str(obj.__origin__).split('.')[-1]
161
- elif type(obj)==property: return _get_property_name(obj)
162
- elif callable(obj): return type(obj).__name__
163
- else: return str(obj).split('.')[-1]
164
-
165
-
166
- # %% ../nbs/04_docments.ipynb #5e273f22
167
- def qual_name(obj):
168
- "Get the qualified name of `obj`"
169
- if hasattr(obj,'__qualname__'): return obj.__qualname__
170
- if ismethod(obj): return f"{get_name(obj.__self__)}.{get_name(fn)}"
171
- return get_name(obj)
172
-
173
163
  # %% ../nbs/04_docments.ipynb #2e865627
174
164
  def ann_parts(anno):
175
165
  "The underlying type and metadata tuple of an `Annotated`, else `(anno, ())`"
@@ -178,9 +168,16 @@ def ann_parts(anno):
178
168
  t = None if args[0] is type(None) else args[0]
179
169
  return t, args[1:]
180
170
 
171
+ # %% ../nbs/04_docments.ipynb #1b4d817c
172
+ def _merge_docs(dms, npdocs):
173
+ for nm,dm in dms.items():
174
+ npdoc = npdocs['Returns'] if nm=='return' else npdocs['Parameters'].get(nm)
175
+ if npdoc and not dm.docment: dm.docment = '\n'.join(npdoc.desc)
176
+ return dms
177
+
181
178
  # %% ../nbs/04_docments.ipynb #9b62ab20
182
179
  def docments(s, full=False, eval_str=False, returns=True, args_kwargs=False):
183
- "Get docments for `s`"
180
+ "Get parameter and return documentation from comments, `Annotated` metadata, and NumPy docstrings"
184
181
  if isclass(s) and not is_dataclass(s): s = s.__init__
185
182
  try: sig = signature_ex(s, eval_str=eval_str)
186
183
  except (ValueError, TypeError): return AttrDict()
@@ -192,18 +189,13 @@ def docments(s, full=False, eval_str=False, returns=True, args_kwargs=False):
192
189
  for k,v in p.items():
193
190
  if v not in docs: docs[v] = _get_comment(k, v, c, p)
194
191
  s = getattr(s, '__delwrap__', None)
195
-
196
- res = {k:_get_full(v, docs, eval_str=eval_str) if full else docs.get(k) for k,v in sig.parameters.items()}
197
- if returns:
198
- if full: res['return'] = AttrDict(docment=docs.get('return'), anno=sig.return_annotation, default=empty)
199
- else: res['return'] = docs.get('return')
200
- for k,p in sig.parameters.items():
201
- if (res[k].docment if full else res[k]) is None:
202
- am = first(o for o in ann_parts(p.annotation)[1] if isinstance(o,str))
203
- if am is None: continue
204
- if full: res[k].docment = am
205
- else: res[k] = am
206
- return AttrDict(_merge_docs(res, nps))
192
+
193
+ res = {k:_get_full(v, docs, eval_str=eval_str) for k,v in sig.parameters.items()}
194
+ if returns: res['return'] = AttrDict(docment=docs.get('return'), anno=sig.return_annotation, default=empty)
195
+ for dm in res.values():
196
+ if dm.docment is None: dm.docment = first(o for o in ann_parts(dm.anno)[1] if isinstance(o,str))
197
+ res = _merge_docs(res, nps)
198
+ return AttrDict(res if full else {k:v.docment for k,v in res.items()})
207
199
 
208
200
  # %% ../nbs/04_docments.ipynb #40cdbeb2
209
201
  def sig_source(obj):
@@ -221,7 +213,7 @@ def _get_params(node):
221
213
  if node.args.kwarg: params.append(f"**{node.args.kwarg.arg}")
222
214
  return ", ".join(params)
223
215
 
224
- # %% ../nbs/04_docments.ipynb #180b4c2d
216
+ # %% ../nbs/04_docments.ipynb #5dc1b730
225
217
  class _DocstringExtractor(ast.NodeVisitor):
226
218
  def __init__(self): self.docstrings,self.cls,self.cls_init = {},None,None
227
219
 
@@ -252,7 +244,7 @@ class _DocstringExtractor(ast.NodeVisitor):
252
244
  if module_doc: self.docstrings['_module'] = (module_doc, "")
253
245
  self.generic_visit(node)
254
246
 
255
- # %% ../nbs/04_docments.ipynb #b1d612e9
247
+ # %% ../nbs/04_docments.ipynb #ac56083a
256
248
  def extract_docstrings(code):
257
249
  "Create a dict from function/class/method names to tuples of docstrings and param lists"
258
250
  extractor = _DocstringExtractor()
@@ -260,21 +252,19 @@ def extract_docstrings(code):
260
252
  return extractor.docstrings
261
253
 
262
254
  # %% ../nbs/04_docments.ipynb #e4e1b815
263
- def _non_empty_keys(d:dict): return L([k for k,v in d.items() if v != inspect._empty])
264
255
  def _bold(s): return f'**{s}**' if s.strip() else s
265
256
 
257
+ def _maybe_nm(o):
258
+ if (o == inspect._empty): return ''
259
+ else: return o.__name__ if hasattr(o, '__name__') else str(o)
260
+
266
261
  # %% ../nbs/04_docments.ipynb #ac070254
267
262
  def _escape_markdown(s):
268
263
  for c in '|^': s = re.sub(rf'\\?\{c}', rf'\{c}', s)
269
264
  return s.replace('\n', '<br>')
270
265
 
271
- # %% ../nbs/04_docments.ipynb #ced78f56
272
- def _maybe_nm(o):
273
- if (o == inspect._empty): return ''
274
- else: return o.__name__ if hasattr(o, '__name__') else str(o)
275
-
276
266
  # %% ../nbs/04_docments.ipynb #fe6d83f1
277
- def _list2row(l:list): return '| '+' | '.join([_maybe_nm(o) for o in l]) + ' |'
267
+ def _list2row(l:list): return '| '+' | '.join(_escape_markdown(_maybe_nm(o)) for o in l) + ' |'
278
268
 
279
269
  # %% ../nbs/04_docments.ipynb #44ac2e4f
280
270
  class _DocmentBase:
@@ -285,49 +275,42 @@ class _DocmentBase:
285
275
  @property
286
276
  def has_docment(self): return any(v.get('docment') for v in self.dm.values())
287
277
 
288
- # %% ../nbs/04_docments.ipynb #b45f731d
278
+ # %% ../nbs/04_docments.ipynb #f7c36bf3
289
279
  class DocmentTbl(_DocmentBase):
290
- _map = {'anno':'Type', 'default':'Default', 'docment':'Details'}
280
+ _map = dict(anno='Type', default='Default', docment='Details')
291
281
 
292
282
  def __init__(self, obj, verbose=True, returns=True):
293
283
  "Compute the docment table string"
294
284
  super().__init__(obj)
295
285
  self.verbose = verbose
296
286
  self.returns = False if isdataclass(obj) else returns
297
- try: self.params = L(signature(obj, eval_str=True).parameters.keys())
298
- except (ValueError,TypeError): self.params=[]
287
+ self.params = L(k for k in self.dm if k!='return')
299
288
  for d in self.dm.values(): d['docment'] = ifnone(d['docment'], inspect._empty)
300
289
 
301
290
  @property
302
291
  def _columns(self):
303
- "Compute the set of fields that have at least one non-empty value so we don't show tables empty columns"
304
- cols = set(flatten(L(self.dm.values()).filter().map(_non_empty_keys)))
292
+ "Fields with at least one non-empty value"
305
293
  candidates = self._map if self.verbose else {'docment': 'Details'}
306
- return {k:v for k,v in candidates.items() if k in cols}
294
+ return {k:v for k,v in candidates.items() if any(d[k] != empty for d in self.dm.values())}
307
295
 
308
296
  @property
309
- def has_docment(self): return 'docment' in self._columns and self._row_list
297
+ def has_docment(self): return 'docment' in self._columns and bool(self.params)
310
298
 
311
299
  @property
312
- def has_return(self): return self.returns and bool(_non_empty_keys(self.dm.get('return', {})))
313
-
314
- def _row(self, nm, props): return [nm] + [props[c] for c in self._columns]
315
-
316
- @property
317
- def _row_list(self):
318
- ordered_params = [(p, self.dm[p]) for p in self.params if p != 'self' and p in self.dm]
319
- return L([self._row(nm, props) for nm,props in ordered_params])
320
-
321
- @property
322
- def _hdr_list(self): return [' '] + [_bold(l) for l in L(self._columns.values())]
300
+ def has_return(self): return self.returns and any(v != empty for v in self.dm.get('return', {}).values())
323
301
 
324
302
  @property
325
303
  def hdr_str(self):
326
- md = _list2row(self._hdr_list)
327
- return md + '\n' + _list2row(['-' * len(l) for l in self._hdr_list])
304
+ hdr = [' '] + [_bold(v) for v in self._columns.values()]
305
+ return _list2row(hdr) + '\n' + _list2row(['---'] * len(hdr))
328
306
 
329
307
  @property
330
- def params_str(self): return '\n'.join(self._row_list.map(_list2row))
308
+ def params_str(self):
309
+ cols,rows = self._columns,[]
310
+ for nm in self.params:
311
+ row = [nm] + [self.dm[nm][c] for c in cols]
312
+ rows.append(_list2row(row))
313
+ return '\n'.join(rows)
331
314
 
332
315
  @property
333
316
  def return_str(self): return _list2row(['**Returns**']+[_bold(_maybe_nm(self.dm['return'][c])) for c in self._columns])
@@ -342,6 +325,7 @@ class DocmentTbl(_DocmentBase):
342
325
  __str__ = _repr_markdown_
343
326
  __repr__ = basic_repr()
344
327
 
328
+
345
329
  # %% ../nbs/04_docments.ipynb #5af61ab1
346
330
  class DocmentList(_DocmentBase):
347
331
  def _fmt(self, nm, p):
@@ -353,12 +337,6 @@ class DocmentList(_DocmentBase):
353
337
  def _repr_markdown_(self): return '\n'.join(self._fmt(k,v) for k,v in self.dm.items())
354
338
  __repr__=__str__=_repr_markdown_
355
339
 
356
- # %% ../nbs/04_docments.ipynb #6355b569
357
- def _clean_text_sig(obj):
358
- if not (sig := getattr(obj, '__text_signature__', None)): return None
359
- sig = re.sub(r'\$\w+,?\s*', '', sig)
360
- return get_name(obj) + sig.replace('<unrepresentable>', '...')
361
-
362
340
  # %% ../nbs/04_docments.ipynb #cfcc46bf
363
341
  def _fmt_sig(name, params, ret_str, maxline, prefix='def'):
364
342
  "Format function signature with params and docment comments"
@@ -369,7 +347,9 @@ def _fmt_sig(name, params, ret_str, maxline, prefix='def'):
369
347
  lines.append(', '.join(curr) + ',')
370
348
  curr = []
371
349
  curr.append(fmt)
372
- if doc: lines.append(', '.join(curr) + ',' + comment); curr = []
350
+ if doc:
351
+ lines.append(', '.join(curr) + ',' + comment)
352
+ curr = []
373
353
  if curr: lines.append(', '.join(curr))
374
354
  pstr = '\n '.join(lines)
375
355
  if not pstr: return f"{prefix} {name}({ret_str}"
@@ -381,6 +361,11 @@ def _fmt_default(o):
381
361
  return o.__name__ if hasattr(o, '__name__') else repr(o)
382
362
 
383
363
  # %% ../nbs/04_docments.ipynb #7f5e5282
364
+ def _type_str(anno):
365
+ anno = ann_parts(anno)[0]
366
+ return inspect.formatannotation(anno).removeprefix('typing.').strip("'")
367
+
368
+ # %% ../nbs/04_docments.ipynb #4c78c84f
384
369
  class DocmentText(_DocmentBase):
385
370
  def __init__(self, obj, maxline=110, docstring=True):
386
371
  super().__init__(obj)
@@ -389,46 +374,79 @@ class DocmentText(_DocmentBase):
389
374
  def _fmt_param(self, nm, p):
390
375
  anno,default,kind = p.get('anno',empty), p.get('default',empty), p.get('kind')
391
376
  pre = '*' if kind==Parameter.VAR_POSITIONAL else '**' if kind==Parameter.VAR_KEYWORD else ''
392
- return pre + nm + (f':{_maybe_nm(anno)}' if anno != empty else '') + (f'={_fmt_default(default)}' if default != empty else '')
393
-
394
- @property
395
- def _ret_str(self):
396
- ret = self.dm.get('return', {})
397
- anno = f"->{_maybe_nm(ret.get('anno',empty))}" if ret.get('anno',empty) != empty else ''
398
- doc = f" # {ret['docment']}" if ret.get('docment') else ''
399
- return f"){anno}:{doc}"
400
-
401
- @property
402
- def params(self): return [(self._fmt_param(k,v), v.get('docment','')) for k,v in self.dm.items() if k != 'return']
403
-
404
- def __str__(self):
405
- o = self.obj
406
- if not callable(o):
407
- doc = docstring(o)
408
- docstr = f'\n "{doc}"' if self.docstring and doc else ''
409
- return f"{type(o).__name__} instance: {truncstr(repr(o), 80)}{docstr}"
410
- is_inst = callable(o) and not (isfunction(o) or isclass(o) or inspect.isbuiltin(o) or ismethod(o))
411
- prefix = 'async def' if inspect.iscoroutinefunction(o) else 'def'
412
- nm = get_name(o) if hasattr(o, '__name__') else f'{type(o).__name__}.__call__' if is_inst else get_name(o)
413
- if (sig := _clean_text_sig(o)) and not self.params: sig_str = f"{prefix} {sig}"
414
- else: sig_str = _fmt_sig(nm, self.params, self._ret_str, self.maxline, prefix=prefix)
415
- doc = getattr(o.__call__, '__doc__', None) if is_inst else o.__doc__
416
- docstr = f' "{doc}"' if self.docstring and doc else ''
417
- return f"{sig_str}\n{docstr}" if docstr else sig_str
418
-
419
- __repr__ = __str__
377
+ anno = f':{_type_str(anno)}' if anno != empty else ''
378
+ default = f'={_fmt_default(default)}' if default != empty else ''
379
+ return pre + nm + anno + default
380
+
420
381
  def _repr_markdown_(self): return f"```python\n{self}\n```"
421
382
 
383
+ # %% ../nbs/04_docments.ipynb #ae2624ea
384
+ @patch(as_prop=True)
385
+ def params(self:DocmentText):
386
+ res,prev,star = [],None,False
387
+ for k,v in self.dm.items():
388
+ if k=='return': continue
389
+ kind = v.get('kind')
390
+ if prev==Parameter.POSITIONAL_ONLY and kind!=prev: res.append(('/', None))
391
+ if kind==Parameter.KEYWORD_ONLY and not star:
392
+ res.append(('*', None))
393
+ star = True
394
+ if kind==Parameter.VAR_POSITIONAL: star = True
395
+ res.append((self._fmt_param(k,v), v.get('docment','')))
396
+ prev = kind
397
+ if prev==Parameter.POSITIONAL_ONLY: res.append(('/', None))
398
+ return res
399
+
400
+ # %% ../nbs/04_docments.ipynb #35564516
401
+ def _clean_text_sig(obj):
402
+ if not (sig := getattr(obj, '__text_signature__', None)): return None
403
+ sig = re.sub(r'\$\w+,?\s*', '', sig)
404
+ return get_name(obj) + sig.replace('<unrepresentable>', '...')
405
+
406
+ @patch(as_prop=True)
407
+ def _ret_str(self:DocmentText):
408
+ ret = self.dm.get('return', {})
409
+ anno = f"->{_type_str(ret['anno'])}" if ret.get('anno',empty) != empty else ''
410
+ doc = f" # {ret['docment']}" if ret.get('docment') else ''
411
+ return f"){anno}:{doc}"
412
+
413
+ # %% ../nbs/04_docments.ipynb #2309edc7
414
+ @patch
415
+ def __str__(self:DocmentText):
416
+ o = self.obj
417
+ doc = docstring(o)
418
+ docstr = f'\n "{doc}"' if self.docstring and doc else ''
419
+ if not callable(o): return f"{type(o).__name__} instance: {truncstr(repr(o), 80)}{docstr}"
420
+ is_inst = _callable_instance(o)
421
+ async_ = inspect.iscoroutinefunction(o) or (is_inst and inspect.iscoroutinefunction(o.__call__))
422
+ prefix = 'async def' if async_ else 'def'
423
+ nm = f'{type(o).__name__}.__call__' if is_inst and not hasattr(o, '__name__') else get_name(o)
424
+ if (sig := _clean_text_sig(o)) and not self.params: sig_str = f"{prefix} {sig}"
425
+ else: sig_str = _fmt_sig(nm, self.params, self._ret_str, self.maxline, prefix=prefix)
426
+ return sig_str + docstr
427
+
428
+ DocmentText.__repr__ = DocmentText.__str__
429
+
422
430
  # %% ../nbs/04_docments.ipynb #d44a7fe3
423
431
  def sig2str(func, maxline=110):
424
432
  "Generate function signature with docments as comments"
425
433
  return DocmentText(func, maxline=maxline, docstring=False)
426
434
 
427
435
  # %% ../nbs/04_docments.ipynb #07287425
428
- def _docstring(sym):
429
- npdoc = parse_docstring(sym)
430
- return '\n\n'.join([npdoc['Summary'], npdoc['Extended']]).strip()
431
-
436
+ def _docstring(sym, dm):
437
+ doc = docstring(sym)
438
+ parts = re.split(r'(?m)^([\w ][\w ]*)\n[-=]{3,}[ \t]*\n', doc)
439
+ res = [parts[0]]
440
+ for title,body in zip(parts[1::2], parts[2::2]):
441
+ headers = [s for s in body.splitlines() if s and not s[0].isspace()]
442
+ if title=='Parameters' and headers:
443
+ params = [s.partition(':') for s in headers]
444
+ if all(sep and _type_str(dm.get(n.strip(), {}).get('anno', empty))==t.strip() for n,sep,t in params): continue
445
+ if title=='Returns' and len(headers)==1 and headers[0].strip()==_type_str(dm.get('return', {}).get('anno', empty)): continue
446
+ res.append(f'{title}\n{"-"*len(title)}\n{body}')
447
+ return '\n\n'.join(s.strip() for s in res if s.strip())
448
+
449
+ # %% ../nbs/04_docments.ipynb #21ebcfcb
432
450
  def _unwrap_sym(sym):
433
451
  if not hasattr(sym, '__signature__'): sym = getattr(sym, '__wrapped__', sym)
434
452
  return (sym.fget or sym.fset or sym) if isinstance(sym, property) else sym
@@ -436,7 +454,9 @@ def _unwrap_sym(sym):
436
454
  def can_render(sym):
437
455
  "Check if `sym` has a renderable signature"
438
456
  sym = _unwrap_sym(sym)
439
- try: signature(sym, eval_str=True); return True
457
+ try:
458
+ signature(sym, eval_str=True)
459
+ return True
440
460
  except (ValueError, TypeError): return False
441
461
 
442
462
  # %% ../nbs/04_docments.ipynb #9de89cb6
@@ -453,34 +473,21 @@ class ShowDocRenderer:
453
473
  self.isfunc = inspect.isfunction(sym)
454
474
  try: self.sig = signature(sym, eval_str=True)
455
475
  except (ValueError,TypeError): self.sig = None
456
- self.docs = _docstring(sym)
457
476
  try: self.dm = DocmentText(sym, maxline=maxline, docstring=False)
458
477
  except Exception as e:
459
478
  warnings.warn(f'docments unavailable for {self.nm}: {type(e).__name__}: {e}')
460
479
  self.dm = f'{self.nm}{self.sig or "(...)"}'
480
+ self.docs = _docstring(sym, self.dm.dm if isinstance(self.dm, DocmentText) else {})
461
481
  self.fn = _fullname(sym)
462
482
 
463
483
  __repr__ = basic_repr()
464
484
 
465
- # %% ../nbs/04_docments.ipynb #e569d885
466
- def _f_name(o): return f'<function {o.__name__}>' if isinstance(o, FunctionType) else None
467
- def _fmt_anno(o): return inspect.formatannotation(o).strip("'").replace(' ','')
468
-
469
- def _show_param(param):
470
- "Like `Parameter.__str__` except removes: quotes in annos, spaces, ids in reprs"
471
- kind,res,anno,default = param.kind,param._name,param._annotation,param._default
472
- kind = '*' if kind==inspect._VAR_POSITIONAL else '**' if kind==inspect._VAR_KEYWORD else ''
473
- res = kind+res
474
- if anno is not inspect._empty: res += f':{_f_name(anno) or _fmt_anno(anno)}'
475
- if default is not inspect._empty: res += f'={_f_name(default) or repr(default)}'
476
- return res
477
-
478
485
  # %% ../nbs/04_docments.ipynb #59797eb7
479
486
  def _ital_first(s:str):
480
487
  "Surround first line with * for markdown italics, preserving leading spaces"
481
488
  return re.sub(r'^(\s*)(.+)', r'\1*\2*', s, count=1)
482
489
 
483
- # %% ../nbs/04_docments.ipynb #9184b175
490
+ # %% ../nbs/04_docments.ipynb #c2aa24dd
484
491
  class MarkdownRenderer(ShowDocRenderer):
485
492
  "Markdown renderer for `show_doc`"
486
493
  def _repr_markdown_(self):
@@ -17,9 +17,9 @@ __all__ = ['langs', 'cell_insert_line', 'cell_str_replace', 'cell_strs_replace',
17
17
  'first_code_ln', 'dir_tag', 'nb2dict', 'nb2str', 'write_nb', 'find_id', 'cell_edit', 'view_cell',
18
18
  'diff_cells', 'validate_cell', 'validate_nb', 'repair_cell', 'repair_nb', 'preferred_out', 'join_out',
19
19
  'mk_stream', 'mk_result', 'mk_display', 'mk_error', 'concat_streams', 'preferred_msg_out', 'render_output',
20
- 'render_outputs', 'render_text', 'item2xml', 'cell2xml', 'cells2xml', 'Notebook', 'CellRow', 'CellRows',
21
- 'summary_nb', 'Found', 'FoundCells', 'find_cells', 'deep_merge', 'update_cell', 'fm_default_eval',
22
- 'does_cell_eval', 'select_cells', 'run_cell', 'msg2out', 'msgs2outs']
20
+ 'render_outputs', 'render_text', 'item2xml', 'cell2xml', 'cells2xml', 'Notebook', 'prev_line', 'CellRow',
21
+ 'CellRows', 'summary_nb', 'Found', 'FoundCells', 'find_cells', 'deep_merge', 'update_cell',
22
+ 'fm_default_eval', 'does_cell_eval', 'select_cells', 'run_cell', 'msg2out', 'msgs2outs']
23
23
 
24
24
  # %% ../nbs/13_nbio.ipynb #954ca1aa
25
25
  from .basics import *
@@ -447,7 +447,7 @@ def validate_cell(cell, idx=None):
447
447
  ct = cell.get('cell_type')
448
448
  if ct not in ('code','markdown','raw'): raise ValueError(f"{where}: unknown cell_type {ct!r}")
449
449
  if not _is_text(cell.get('source', '')): raise ValueError(f"{where}: source must be str or list of str")
450
- if not isinstance(cell.get('metadata', {}), dict): raise ValueError(f"{where}: metadata must be a dict")
450
+ if not isinstance(cell.get('metadata'), dict): raise ValueError(f"{where}: metadata must be a dict")
451
451
  if ct=='code':
452
452
  if not isinstance(cell.get('outputs'), list): raise ValueError(f"{where}: code cell requires an outputs list")
453
453
  if 'execution_count' not in cell: raise ValueError(f"{where}: code cell requires execution_count")
@@ -472,9 +472,9 @@ def repair_cell(cell, idx=None):
472
472
  if not _is_text(src):
473
473
  cell['source'] = ''.join(map(str, listify(src)))
474
474
  res.append(f"{where}: coerced source to text")
475
- if not isinstance(cell.get('metadata', {}), dict):
475
+ if not isinstance(cell.get('metadata'), dict):
476
476
  cell['metadata'] = {}
477
- res.append(f"{where}: reset non-dict metadata")
477
+ res.append(f"{where}: set metadata")
478
478
  if cell.get('cell_type')=='code':
479
479
  if not isinstance(cell.get('outputs'), list):
480
480
  cell['outputs'] = []
@@ -759,6 +759,14 @@ def view_cell(self:Notebook, id, nums=True, incl_out=False, trunc_out=True):
759
759
  # %% ../nbs/13_nbio.ipynb #4bf3dc2e
760
760
  MAXLEN = 180 # Most characters shown per displayed line
761
761
 
762
+ # %% ../nbs/13_nbio.ipynb #80541be8
763
+ def prev_line(txt, maxlen=MAXLEN, pre='', sep=''):
764
+ "One-line preview `pre`+`sep`+`txt` capped at `maxlen`, blank-line runs shown as `¶`; a cut line carries `txt`'s size before `sep` and ends with the count of characters missing"
765
+ n = len(txt)
766
+ txt = re.sub(r'\n(?:\s*\n)*', '¶', txt)
767
+ if len(pre)+len(sep)+len(txt)<=maxlen: return pre+sep+txt
768
+ return truncstr(f'{pre}[{humanize(n)}]{sep}{txt}', maxlen, suf=lambda n: f'…[{humanize(n)}]')
769
+
762
770
  # %% ../nbs/13_nbio.ipynb #804670bc
763
771
  class CellRow:
764
772
  "Snapshot of one cell, shown as `id:t[directives]:source` (t: c=code m=markdown r=raw); a context row from a find shows `-` in place of its final `:`"
@@ -766,10 +774,8 @@ class CellRow:
766
774
  self.id,self.cell_type,self.source,self.maxlen,self.kind = c.id,c.cell_type,c.source,maxlen,kind
767
775
  self.meta = copy.deepcopy(dict(c.get('metadata',{})))
768
776
  def __repr__(self):
769
- src = re.sub(r'\n(?:\s*\n)*', '¶', self.source)
770
- if len(src)>self.maxlen: src = src[:self.maxlen]+'…'
771
777
  sep = ':' if self.kind=='match' else '-'
772
- return f"{self.id}:{self.cell_type[0]}{dir_tag(self.meta)}{sep}{src}"
778
+ return prev_line(self.source, self.maxlen, f"{self.id}:{self.cell_type[0]}{dir_tag(self.meta)}", sep)
773
779
 
774
780
  class CellRows(list):
775
781
  def __repr__(self): return '\n'.join(repr(o) for o in self)
@@ -296,7 +296,7 @@ def untar_dir(fname, dest, rename=False, overwrite=False, uid=-1, gid=-1):
296
296
  if uid>-1 or gid>-1:
297
297
  os.chown(dest, uid, gid)
298
298
  if dest.is_dir():
299
- for p in dest.rglob('*'): os.chown(p, uid, gid)
299
+ for p in dest.rglob('*'): os.chown(p, uid, gid, follow_symlinks=False)
300
300
  return dest
301
301
 
302
302
  # %% ../nbs/03_xtras.ipynb #ab91ee87
@@ -978,10 +978,15 @@ def partial_format(s:str, **kwargs):
978
978
  return res,list(fmt.missing),fmt.xtra
979
979
 
980
980
  # %% ../nbs/03_xtras.ipynb #becf7a2d
981
- def truncstr(s:str, maxlen:int, suf:str='…', space='', sizevar:str=None)->str:
982
- "Truncate `s` to length `maxlen`, adding suffix `suf` if truncated"
981
+ def truncstr(s:str, maxlen:int, suf='…', space='', sizevar:str=None)->str:
982
+ "Truncate `s` to length `maxlen`, adding suffix `suf` if truncated; a callable `suf` gets the number of characters cut"
983
+ if len(s)+len(space)<=maxlen: return s+space
983
984
  if sizevar: suf = suf.format_map({sizevar: len(s)})
984
- return s[:maxlen-len(suf)]+suf if len(s)+len(space)>maxlen else s+space
985
+ if callable(suf):
986
+ f,n = suf,0
987
+ for _ in range(3): n = len(s)-maxlen+len(f(n)) # the count's digits change the suffix length: iterate to a fixed point
988
+ suf = f(n)
989
+ return s[:maxlen-len(suf)]+suf
985
990
 
986
991
  # %% ../nbs/03_xtras.ipynb #a67b3fca
987
992
  def trunc_ctr(s:str, mx:int=1000)->str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastcore
3
- Version: 2.2.22
3
+ Version: 2.2.23
4
4
  Summary: Python supercharged for fastai development
5
5
  Author-email: Jeremy Howard and Sylvain Gugger <infos@fast.ai>
6
6
  License: Apache-2.0
@@ -54,7 +54,7 @@ Here are some tips on using fastcore:
54
54
  - **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path).
55
55
  - **Flexible data structures**: Convert between dictionaries and attribute-access objects using [`dict2obj`](https://fastcore.fast.ai/xtras.html#dict2obj) and [`obj2dict`](https://fastcore.fast.ai/xtras.html#obj2dict) for more intuitive data handling.
56
56
  - **Functional programming paradigms**: Use tools like [`compose`](https://fastcore.fast.ai/basics.html#compose), [`maps`](https://fastcore.fast.ai/basics.html#maps), and [`filter_ex`](https://fastcore.fast.ai/basics.html#filter_ex) to write more functional-style Python code.
57
- - **Documentation**: Use [`docments`](https://fastcore.fast.ai/docments.html#docments) where possible to document parameters of functions and methods.
57
+ - **Documentation**: Document parameters and return values with [`docments`](https://fastcore.fast.ai/docments.html#docments), using source comments or `Annotated` metadata for generated APIs. [`MarkdownRenderer`](https://fastcore.fast.ai/docments.html#markdownrenderer) shows the effective signature and retains usage sections such as Notes, Raises, and Examples.
58
58
  - **Time-aware caching**: Apply the [`timed_cache`](https://fastcore.fast.ai/xtras.html#timed_cache) decorator to add time-based expiration to the standard `lru_cache` functionality.
59
59
  - **Simplified CLI creation**: Use `fastcore.script` to easily transform Python functions into command-line interfaces.
60
60
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes