├── .changelog
└── 2.2.2.toml
├── .envrc
├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── python.yml
├── .gitignore
├── LICENSE
├── docs
├── README.md
├── build.novella
├── content
│ ├── api
│ │ ├── docspec-python.md
│ │ └── docspec.md
│ ├── changelog
│ │ ├── docspec-python.md
│ │ └── docspec.md
│ ├── index.md
│ └── specification.md
├── pyproject.toml
├── scripts
│ └── render-spec
└── uv.lock
├── docspec-python
├── .changelog
│ ├── 0.0.5.toml
│ ├── 0.0.6.toml
│ ├── 0.0.7.toml
│ ├── 0.1.0.toml
│ ├── 0.1.1.toml
│ ├── 0.2.0.toml
│ ├── 1.0.0.toml
│ ├── 1.1.0.toml
│ ├── 1.1.1.toml
│ ├── 1.2.0.toml
│ ├── 1.3.0.toml
│ ├── 2.0.0a1.toml
│ ├── 2.0.1.toml
│ ├── 2.0.2.toml
│ ├── 2.1.0.toml
│ └── 2.1.1.toml
├── .flake8
├── pyproject.toml
├── readme.md
├── src
│ └── docspec_python
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ ├── parser.py
│ │ └── py.typed
└── test
│ ├── src
│ └── pep420_namespace_package
│ │ └── module.py
│ ├── test_loader.py
│ └── test_parser.py
├── docspec
├── .changelog
│ ├── 0.2.0.toml
│ ├── 0.2.1.toml
│ ├── 1.0.0.toml
│ ├── 1.0.1.toml
│ ├── 1.0.2.toml
│ ├── 1.1.0.toml
│ ├── 1.2.0.toml
│ ├── 2.0.0a1.toml
│ ├── 2.1.2.toml
│ ├── 2.2.0.toml
│ └── 2.2.1.toml
├── .flake8
├── pyproject.toml
├── readme.md
├── specification.yml
├── src
│ └── docspec
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── py.typed
└── test
│ ├── __init__.py
│ ├── conftest.py
│ └── docspec
│ ├── __init__.py
│ └── test_deserialize.py
├── flake.lock
├── flake.nix
├── pyproject.toml
├── readme.md
└── uv.lock
/.changelog/2.2.2.toml:
--------------------------------------------------------------------------------
1 | release-date = "2025-05-06"
2 |
3 | [[entries]]
4 | id = "7e6f481f-0e4b-4502-981c-cf98a757dd6d"
5 | type = "improvement"
6 | description = "Bump black to `>=23.1.0`"
7 | author = "@mcintel"
8 |
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 |
2 | if command -v nix-shell &>/dev/null; then
3 | watch_file flake.nix flake.lock
4 | use flake
5 | fi
6 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to docspec
2 |
3 | Thanks for your interest in contributing to docspec! Contributions are welcome.
4 |
5 | ## Issues vs Discussions
6 |
7 | Please report bugs or feature requests via [Issues](https://github.com/NiklasRosenstein/docspec/issues). For questions, please use [Discussions](https://github.com/NiklasRosenstein/docspec/discussions).
8 |
9 | ## Creating pull requests
10 |
11 | Your pull request should contain a changelog entry for your change in `.changelog/_unreleased.yml`.
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: 'type: bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: 'type: feature request'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
--------------------------------------------------------------------------------
/.github/workflows/python.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Python package
5 |
6 | on:
7 | push: { branches: [ "develop" ], tags: [ "*" ] }
8 | pull_request: { branches: [ "develop" ] }
9 |
10 | jobs:
11 |
12 | test:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | python-version: ["3.8", "3.9", "3.10", "3.x"]
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Check Nix flake Nixpkgs inputs
21 | uses: DeterminateSystems/flake-checker-action@main
22 | - uses: cachix/install-nix-action@v30
23 | with:
24 | nix_path: nixpkgs=channel:nixos-unstable
25 | - uses: cachix/cachix-action@v15
26 | with:
27 | name: mycache
28 |
29 | - run: nix run .#lint
30 | - run: DOCSPEC_TEST_NO_DEVELOP=true nix run .#test
31 |
32 | docs:
33 | permissions:
34 | contents: write
35 | runs-on: ubuntu-latest
36 | steps:
37 | - uses: actions/checkout@v4
38 | - name: Check Nix flake Nixpkgs inputs
39 | uses: DeterminateSystems/flake-checker-action@main
40 | - uses: cachix/install-nix-action@v30
41 | with:
42 | nix_path: nixpkgs=channel:nixos-unstable
43 | - uses: cachix/cachix-action@v15
44 | with:
45 | name: mycache
46 |
47 | - run: nix run .#docs
48 | - name: Publish documentation
49 | uses: peaceiris/actions-gh-pages@v4
50 | with:
51 | personal_token: ${{ secrets.GITHUB_TOKEN }}
52 | publish_dir: ./docs/_site
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.pytest_cache
2 | /.venv
3 | /.vscode
4 | /dist
5 | *.py[cod]
6 | *.egg-info
7 | *.egg
8 | build/
9 | .vscode/
10 | .python-version
11 | docs/_site
12 |
13 | # Generate files for documentation.
14 | changelog.md
15 | spec.md
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2022 Niklas Rosenstein
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | this software and associated documentation files (the "Software"), to deal in
7 | Software without restriction, including without limitation the rights to use,
8 | modify, merge, publish, distribute, sublicense, and/or sell copies of the
9 | and to permit persons to whom the Software is furnished to do so, subject to
10 | following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
17 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
18 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
20 | USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NiklasRosenstein/python-docspec/61d3e38c55d290da6197236357e1cdfe818d35b5/docs/README.md
--------------------------------------------------------------------------------
/docs/build.novella:
--------------------------------------------------------------------------------
1 | template "mkdocs"
2 |
3 | action "mkdocs-update-config" {
4 | site_name = "docspec"
5 | update '$.theme.features' add: ['navigation.sections']
6 | update '$.theme.palette' set: {'scheme': 'slate', 'primary': 'light green', 'accent': 'pink'}
7 | update '$.nav' set: [
8 | 'index.md',
9 | 'specification.md',
10 | { 'API': [
11 | { 'docspec': 'api/docspec.md' },
12 | { 'docspec-python': 'api/docspec-python.md' },
13 | ]},
14 | { 'Changelog': [
15 | { 'docspec': 'changelog/docspec.md' },
16 | { 'docspec-python': 'changelog/docspec-python.md' },
17 | ]}
18 | ]
19 | }
20 |
21 | action "preprocess-markdown" {
22 | use "pydoc" {
23 | loader().search_path = [ '../docspec/src', '../docspec-python/src' ]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/docs/content/api/docspec-python.md:
--------------------------------------------------------------------------------
1 | @pydoc docspec_python
2 |
--------------------------------------------------------------------------------
/docs/content/api/docspec.md:
--------------------------------------------------------------------------------
1 | @pydoc docspec
2 |
--------------------------------------------------------------------------------
/docs/content/changelog/docspec-python.md:
--------------------------------------------------------------------------------
1 | # Docspec-Python Changelog
2 |
3 | @shell cd ../docspec-python && slap changelog format --all --markdown
4 |
--------------------------------------------------------------------------------
/docs/content/changelog/docspec.md:
--------------------------------------------------------------------------------
1 | # Docspec Changelog
2 |
3 | @shell cd ../docspec && slap changelog format --all --markdown
4 |
--------------------------------------------------------------------------------
/docs/content/index.md:
--------------------------------------------------------------------------------
1 | # Welcome to the Docspec documentation!
2 |
3 | @cat ../../readme.md :with slice_lines = "2:"
4 |
--------------------------------------------------------------------------------
/docs/content/specification.md:
--------------------------------------------------------------------------------
1 | # Specification
2 |
3 | @shell scripts/render-spec ../docspec/specification.yml
4 |
--------------------------------------------------------------------------------
/docs/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "docs"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.8"
7 | dependencies = [
8 | "databind>=1.5.0,<4",
9 | "mako>=1.3.6",
10 | "mkdocs>=1.6.1",
11 | "mkdocs-material>=9.5.47",
12 | "novella==0.1.15",
13 | "pydoc-markdown>=4.6.0",
14 | "setuptools>=75.3.0",
15 | ]
16 |
--------------------------------------------------------------------------------
/docs/scripts/render-spec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import dataclasses
4 | import sys
5 | import typing as t
6 |
7 | import databind.core.annotations as annotations
8 | import databind.json
9 | import mako.template
10 | import typing_extensions as te
11 | import yaml
12 |
13 | TEMPLATE = r'''
14 | <%def name="struct(k, s)">
15 | ${'##'} Struct `${k}`
16 |
17 | ${s.docs or ''}
18 |
19 | | Field | Type | Required | Description |
20 | | ----- | ---- | -------- | ----------- |
21 | % for name, field in s.fields.items():
22 | | `${name}` | `${field.type}` | ${'Yes' if field.required else 'No'} | ${field.docs or ''} |
23 | % endfor
24 | %def>
25 |
26 | <%def name="enum(k, s)">
27 | ${'##'} Enumeration `${k}`
28 |
29 | ${s.docs or ''}
30 |
31 | % for value in s.values:
32 | * `${value.name}` – ${value.docs or ''}
33 | % endfor
34 | %def>
35 |
36 | % for name, obj in config.items():
37 | % if isinstance(obj, Struct):
38 | ${struct(name, obj)}
39 | % elif isinstance(obj, Enum):
40 | ${enum(name, obj)}
41 | % endif
42 | % endfor
43 | '''
44 |
45 |
46 | @dataclasses.dataclass
47 | class StructField:
48 | type: str
49 | required: bool = True
50 | docs: t.Optional[str] = None
51 |
52 |
53 | @dataclasses.dataclass
54 | class Struct:
55 | fields: t.Dict[str, StructField]
56 | docs: t.Optional[str] = None
57 |
58 |
59 | @dataclasses.dataclass
60 | class EnumValue:
61 | name: str
62 | docs: t.Optional[str] = None
63 |
64 |
65 | @dataclasses.dataclass
66 | class Enum:
67 | values: t.List[EnumValue]
68 | docs: t.Optional[str] = None
69 |
70 |
71 | Config = t.Dict[str, te.Annotated[
72 | t.Union[Struct, Enum],
73 | annotations.union({ 'struct': Struct, 'enum': Enum }, style=annotations.union.Style.flat)]]
74 |
75 |
76 | def main():
77 | with open(sys.argv[1]) as fp:
78 | config = databind.json.load(yaml.safe_load(fp), Config, filename=fp.name)
79 |
80 | template = mako.template.Template(TEMPLATE)
81 | print(template.render(config=config, Struct=Struct, Enum=Enum))
82 |
83 |
84 | if __name__ == '__main__':
85 | main()
86 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/0.0.5.toml:
--------------------------------------------------------------------------------
1 | release-date = "2020-07-06"
2 |
3 | [[entries]]
4 | id = "0a1bc990-7c55-4d46-a4ad-426d74026648"
5 | type = "improvement"
6 | description = "Update calls to `ApiObject` subclasses to pass keyword arguments only (as is required in `docspec >=0.2.0`)"
7 | author = "@NiklasRosenstein"
8 |
9 | [[entries]]
10 | id = "35645f74-4720-4ca1-b15d-94f63a8fe4a8"
11 | type = "fix"
12 | description = "Fix derivation of Python module name from name of file on disk (before it would accidentally strip trailing p's or y's from the name)."
13 | author = "@NiklasRosenstein"
14 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/0.0.6.toml:
--------------------------------------------------------------------------------
1 | release-date = "2020-07-16"
2 |
3 | [[entries]]
4 | id = "900c66ef-ec86-48c4-a9d1-aeb3eb85d0f0"
5 | type = "fix"
6 | description = "fix `iter_package_files()` which would not respect the `search_path` argument"
7 | author = "@NiklasRosenstein"
8 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/0.0.7.toml:
--------------------------------------------------------------------------------
1 | release-date = "2020-07-16"
2 |
3 | [[entries]]
4 | id = "242ab83f-10a2-4850-8094-74dec7da895e"
5 | type = "tests"
6 | description = "fix test cases"
7 | author = "@NiklasRosenstein"
8 |
9 | [[entries]]
10 | id = "87683c2e-2060-417d-94d9-c838a2f1de59"
11 | type = "improvement"
12 | description = "no longer use `pkgutil.find_loader()` to find Python modules as it prefers modules that are already in `sys.modules` even if that instance of the module would not occurr in the specified `search_path`. `docspec_python.find_module()` now re-implements the search mechanism"
13 | author = "@NiklasRosenstein"
14 |
15 | [[entries]]
16 | id = "936b50ea-f597-404f-82ac-d29b2963c001"
17 | type = "tests"
18 | description = "add tests for Python module loader logic"
19 | author = "@NiklasRosenstein"
20 |
21 | [[entries]]
22 | id = "23a36b2f-7cad-4eb8-9709-b2adbf270fd4"
23 | type = "fix"
24 | description = "fix support for Python 3.5"
25 | author = "@NiklasRosenstein"
26 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/0.1.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-02-20"
2 |
3 | [[entries]]
4 | id = "1255a320-a7b9-4deb-aad6-cd9f6f509f37"
5 | type = "fix"
6 | description = "Fix `NameError` in function type annotation"
7 | author = "@NiklasRosenstein"
8 |
9 | [[entries]]
10 | id = "ab9f1070-c89a-4ccb-a431-ff7b2f53d00c"
11 | type = "feature"
12 | description = "add `encoding` parameter to `load_python_modules()` and `parse_python_module()`"
13 | author = "@NiklasRosenstein"
14 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/0.1.1.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-05-21"
2 |
3 | [[entries]]
4 | id = "c23842d4-d6d0-491c-a1e8-4dfd0b0233e9"
5 | type = "improvement"
6 | description = "update type hints, use `@dataclass` over `nr.sumtype` which has MyPy support"
7 | author = "@NiklasRosenstein"
8 |
9 | [[entries]]
10 | id = "7fd34d20-3738-413c-8572-0236fa0a34e4"
11 | type = "fix"
12 | description = "fix `discover()` to ignore Python files with more than one dot in it"
13 | author = "@NiklasRosenstein"
14 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/0.2.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-05-29"
2 |
3 | [[entries]]
4 | id = "ecac32df-3c53-428f-90c4-c026717fd93b"
5 | type = "improvement"
6 | description = "republish 0.1.1 as 0.2.0"
7 | author = "@NiklasRosenstein"
8 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/1.0.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-07-21"
2 |
3 | [[entries]]
4 | id = "8ba6eae4-a4c0-4768-835f-86c96c1fe717"
5 | type = "breaking change"
6 | description = "Migrate to using `databind.core 1.x` and `databind.json 1.x` from `nr.databind.core` and `nr.databind.json`."
7 | author = "@NiklasRosenstein"
8 |
9 | [[entries]]
10 | id = "96c82cdb-5956-4f4c-b82a-20a37bb31144"
11 | type = "fix"
12 | description = "fix parsing of lib2to3 syntax tree if class does not have a suite (e.g., \"pass\" on the same line as the class definition)"
13 | author = "@NiklasRosenstein"
14 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/1.1.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-08-27"
2 |
3 | [[entries]]
4 | id = "8daf7dd8-f03e-4f75-b11f-7c0b3e3a74af"
5 | type = "feature"
6 | description = "add support for the `Docstring` class now used in `ApiObject.docstring` since `docspec 1.1.0`"
7 | author = "@NiklasRosenstein"
8 |
9 | [[entries]]
10 | id = "dd4f2051-f33c-4120-9fc1-29d95be43a38"
11 | type = "feature"
12 | description = "add support for `Indirection`s (from parsed Python imports)"
13 | author = "@NiklasRosenstein"
14 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/1.1.1.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-08-27"
2 |
3 | [[entries]]
4 | id = "4ca2cd58-a280-4983-9d44-7f7afda93f71"
5 | type = "fix"
6 | description = "support imports on the class-level"
7 | author = "@NiklasRosenstein"
8 | issues = [
9 | "https://github.com/NiklasRosenstein/docspec/issues/34",
10 | ]
11 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/1.2.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-09-24"
2 |
3 | [[entries]]
4 | id = "5a76bf89-00e4-4340-a839-25b530866bbc"
5 | type = "feature"
6 | description = "add `format_arglist(render_type_hints)` argument"
7 | author = "@NiklasRosenstein"
8 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/1.3.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2022-02-23"
2 |
3 | [[entries]]
4 | id = "da68f360-f675-4f64-bce4-457963de4809"
5 | type = "fix"
6 | description = "strip whitespace around `Class.bases`"
7 | author = "@NiklasRosenstein"
8 | pr = "https://github.com/NiklasRosenstein/docspec/pulls/55"
9 | issues = [
10 | "https://github.com/NiklasRosenstein/docspec/issues/53",
11 | ]
12 |
13 | [[entries]]
14 | id = "2aa754a8-829e-40b0-947d-ebcb1d72284d"
15 | type = "tests"
16 | description = "add `test_funcdef_7_posonly_args` unit test to test various more combinations of function arguments, including `POSITIONAL_ONLY`)"
17 | authors = [
18 | "@NiklasRosenstein",
19 | "@tristanlatr",
20 | ]
21 | pr = "https://github.com/NiklasRosenstein/docspec/pulls/58"
22 | issues = [
23 | "https://github.com/NiklasRosenstein/docspec/issues/57",
24 | ]
25 |
26 | [[entries]]
27 | id = "baf461c7-737f-4cda-b2fd-1213b7637281"
28 | type = "refactor"
29 | description = "use `nr.util.scanner.Scanner` instead of homebrew `ListScanner` class"
30 | author = "@NiklasRosenstein"
31 | pr = "https://github.com/NiklasRosenstein/docspec/pulls/58"
32 |
33 | [[entries]]
34 | id = "0338523c-6ec7-42f0-aba6-7cf9266302f0"
35 | type = "fix"
36 | description = "fix parsing of positional only arguments"
37 | author = "@NiklasRosenstein"
38 | pr = "https://github.com/NiklasRosenstein/docspec/pulls/58"
39 | issues = [
40 | "https://github.com/NiklasRosenstein/docspec/issues/57",
41 | ]
42 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/2.0.0a1.toml:
--------------------------------------------------------------------------------
1 | release-date = "2022-02-24"
2 |
3 | [[entries]]
4 | id = "043bf8eb-3804-4de2-8d46-ddef40b30c49"
5 | type = "feature"
6 | description = "support PEP 420 namespace packages"
7 | author = "@NiklasRosenstein"
8 | pr = "https://github.com/NiklasRosenstein/docspec/pull/62"
9 | issues = [
10 | "https://github.com/NiklasRosenstein/docspec/issues/54",
11 | ]
12 |
13 | [[entries]]
14 | id = "f773f5b6-3002-45ad-b784-44aaaeca42ad"
15 | type = "improvement"
16 | description = "support Path where a file system path is expected (e.g. `iter_package_files()`, `load_python_modules()`)"
17 | author = "@NiklasRosenstein"
18 | pr = "https://github.com/NiklasRosenstein/docspec/pull/62"
19 | issues = [
20 | "https://github.com/NiklasRosenstein/docspec/issues/61",
21 | ]
22 |
23 | [[entries]]
24 | id = "e3629fc5-e3e3-4c46-a6ab-d5100aa7f72a"
25 | type = "breaking change"
26 | description = "`Docstring` class no longer inherits from `str` and is no longer frozen"
27 | author = "@NiklasRosenstein"
28 | pr = "https://github.com/NiklasRosenstein/docspec/pull/64"
29 | issues = [
30 | "https://github.com/NiklasRosenstein/docspec/issues/49",
31 | ]
32 |
33 | [[entries]]
34 | id = "c97dec50-a34b-49ad-9dc5-60b693524c03"
35 | type = "breaking change"
36 | description = "rename `Data` to `Variable`"
37 | author = "@NiklasRosenstein"
38 | pr = "https://github.com/NiklasRosenstein/docspec/pull/68"
39 | issues = [
40 | "https://github.com/NiklasRosenstein/docspec/issues/67",
41 | ]
42 |
43 | [[entries]]
44 | id = "300029d9-3c6f-43b6-989b-44603803c623"
45 | type = "hygiene"
46 | description = "move `VariableSemantic`, `FunctionSemantic` and `ClassSemantic` into global scope"
47 | author = "@NiklasRosenstein"
48 | pr = "https://github.com/NiklasRosenstein/docspec/pull/69"
49 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/2.0.1.toml:
--------------------------------------------------------------------------------
1 | release-date = "2022-03-24"
2 |
3 | [[entries]]
4 | id = "04bfb316-45e8-41a5-b385-568684e12fbb"
5 | type = "fix"
6 | description = "Fix `format_arglist()` for function signatures that contained a `POSITIONAL_REMAINDER` argument followed by a `KEYWORD_ONLY` (i.e. any other arguments besides `KEYWORD_REMAINDER`) to not yield an additional star (`*`)"
7 | author = "@NiklasRosenstein"
8 | issues = [
9 | "https://github.com/NiklasRosenstein/pydoc-markdown/issues/255",
10 | ]
11 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/2.0.2.toml:
--------------------------------------------------------------------------------
1 | release-date = "2022-07-18"
2 |
3 | [[entries]]
4 | id = "9698224d-eca1-41c5-9cf5-c04d34a607fe"
5 | type = "fix"
6 | description = "fix parsing trailing comma after keyword remainder argument in function definition (before it would accidentally consider the comma as a positional argument)"
7 | author = "@NiklasRosenstein"
8 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/2.1.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2023-03-10"
2 |
3 | [[entries]]
4 | id = "8628524b-3376-45db-a676-240b00c20d08"
5 | type = "fix"
6 | description = "Swap in `blib2to3` parser (bundled with the `black` package) for the stdlib `lib2to3` module in order to support `match` statements (PEP 634 - Structural Pattern Matching)."
7 | author = "@nrser"
8 | pr = "https://github.com/NiklasRosenstein/docspec/pull/80"
9 |
10 | [[entries]]
11 | id = "1e03c529-5dc3-4d7b-a2d9-827376ddeee9"
12 | type = "improvement"
13 | description = "add back `files` as a keyword-only argument to `load_python_modules()`"
14 | author = "@NiklasRosenstein"
15 | issues = [
16 | "https://github.com/NiklasRosenstein/docspec/issues/75",
17 | ]
18 |
19 | [[entries]]
20 | id = "9e84cf49-c0a4-4ee9-87d7-379f082a7d46"
21 | type = "tests"
22 | description = "add testcase for #76"
23 | author = "@NiklasRosenstein"
24 | issues = [
25 | "https://github.com/NiklasRosenstein/docspec/issues/76",
26 | ]
27 |
28 | [[entries]]
29 | id = "8e5b8177-cbb9-4b70-8a54-822f2cda7774"
30 | type = "fix"
31 | description = "Support parsing raw docstrings and decoding escape sequences appropriately using `str.encode(\"latin1\").decode(\"unicode_escape\")` (see https://stackoverflow.com/a/58829514/791713)."
32 | author = "@NiklasRosenstein"
33 | issues = [
34 | "https://github.com/NiklasRosenstein/docspec/issues/71",
35 | ]
36 |
37 | [[entries]]
38 | id = "2e159810-b33a-4c44-936f-ca1f884ff882"
39 | type = "tests"
40 | description = "add test case for parsing docstring _after_ a variable declaration"
41 | author = "@NiklasRosenstein"
42 | issues = [
43 | "https://github.com/NiklasRosenstein/docspec/issues/65",
44 | ]
45 |
46 | [[entries]]
47 | id = "da5abd74-881d-43a9-8e8d-5476f62e54d3"
48 | type = "improvement"
49 | description = "Explicitly do not support tuple-upackings as it is unclear how to assign the associated docstring and value."
50 | author = "@NiklasRosenstein"
51 |
52 | [[entries]]
53 | id = "f71d41d1-cc7a-4dc3-999d-70f94f6a3f80"
54 | type = "feature"
55 | description = "Support docstrings on the same line for variable definitions (ex.: `a: int #: This is the docstring`)."
56 | author = "@NiklasRosenstein"
57 | issues = [
58 | "https://github.com/NiklasRosenstein/docspec/issues/2",
59 | ]
60 |
61 | [[entries]]
62 | id = "4f5e29cd-317c-485b-ba3f-6f56ebb970d8"
63 | type = "fix"
64 | description = "Fix hash docstrings (`#:`) loosing their indentation relative to the rest of the docstring lines."
65 | author = "@NiklasRosenstein"
66 | issues = [
67 | "https://github.com/NiklasRosenstein/docspec/issues/1",
68 | ]
69 |
70 | [[entries]]
71 | id = "74ceb273-f285-416e-baf0-52b4cdab0b81"
72 | type = "tests"
73 | description = "Add unit test for #72"
74 | author = "@NiklasRosenstein"
75 | issues = [
76 | "https://github.com/NiklasRosenstein/docspec/issues/72",
77 | ]
78 |
--------------------------------------------------------------------------------
/docspec-python/.changelog/2.1.1.toml:
--------------------------------------------------------------------------------
1 | release-date = "2023-03-15"
2 |
3 | [[entries]]
4 | id = "a519ecc4-1b48-4883-a930-dc6f7196e54d"
5 | type = "fix"
6 | description = "Fix #91 by using `ast.literal_eval()` instead of `encode(latin1).decode(unicode_escape)` method."
7 | author = "@NiklasRosenstein"
8 | issues = [
9 | "https://github.com/NiklasRosenstein/docspec/issues/91",
10 | ]
11 |
--------------------------------------------------------------------------------
/docspec-python/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 120
3 | # Black can yield formatted code that triggers these Flake8 warnings.
4 | ignore=
5 | # line break before binary operator
6 | W503,
7 | # line break after binary operator
8 | W504,
9 |
--------------------------------------------------------------------------------
/docspec-python/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "docspec-python"
7 | version = "2.2.2"
8 | description = "A parser based on lib2to3 producing docspec data from Python source code."
9 | readme = "readme.md"
10 | requires-python = ">=3.8"
11 | dependencies = [
12 | "black>=24.8.0",
13 | "docspec==2.2.1",
14 | "nr-util>=0.8.12",
15 | ]
16 | authors = [{ name = "Niklas Rosenstein", email = "rosensteinniklas@gmail.com" }]
17 |
18 | [project.scripts]
19 | docspec-python = "docspec_python.__main__:main"
20 |
21 | [tool.uv]
22 | dev-dependencies = [
23 | "mypy>=1.13.0",
24 | "pytest>=8.3.4",
25 | "types-deprecated>=1.2.15.20241117",
26 | "types-termcolor>=1.1.6.2",
27 | ]
28 |
29 | [tool.uv.sources]
30 | docspec = { workspace = true }
31 |
32 | [tool.mypy]
33 | python_version = "3.8"
34 | explicit_package_bases = true
35 | mypy_path = ["src"]
36 | namespace_packages = true
37 | pretty = true
38 | show_error_codes = true
39 | show_error_context = true
40 | strict = true
41 | warn_no_return = true
42 | warn_redundant_casts = true
43 | warn_unreachable = true
44 | warn_unused_ignores = true
45 | check_untyped_defs = true
46 |
47 | [[tool.mypy.overrides]]
48 | module = "blib2to3.*"
49 | ignore_missing_imports = true
50 |
51 | [tool.ruff]
52 | line-length = 120
53 |
--------------------------------------------------------------------------------
/docspec-python/readme.md:
--------------------------------------------------------------------------------
1 | [docspec]: https://github.com/NiklasRosenstein/docspec
2 |
3 | # docspec-python
4 |
5 | A parser based on `lib2to3` procuding [docspec][] data from Python source code.
6 |
7 | Example:
8 |
9 | ```
10 | from docspec_python import parse_python_module
11 | import docspec, sys
12 | docspec.dump_module(parse_python_module(sys.stdin, print_function=False), sys.stdout)
13 | ```
14 |
15 | ```
16 | $ docspec-python -p docspec | docspec --dump-tree --multiple | head
17 | module __init__
18 | | data __author__
19 | | data __version__
20 | | data __all__
21 | | data _ClassProxy
22 | | data _mapper
23 | | class Location
24 | | | data filename
25 | | | data lineno
26 | | class Decoration
27 | ```
28 |
29 | ---
30 |
31 |
Copyright © 2020, Niklas Rosenstein
32 |
--------------------------------------------------------------------------------
/docspec-python/src/docspec_python/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf8 -*-
2 | # Copyright (c) 2020 Niklas Rosenstein
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to
6 | # deal in the Software without restriction, including without limitation the
7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8 | # sell copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in
12 | # all copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20 | # IN THE SOFTWARE.
21 |
22 | __author__ = "Niklas Rosenstein "
23 | __version__ = "2.2.1"
24 | __all__ = [
25 | "Parser",
26 | "ParserOptions",
27 | "load_python_modules",
28 | "parse_python_module",
29 | "find_module",
30 | "iter_package_files",
31 | "DiscoveryResult",
32 | "discover",
33 | ]
34 |
35 | import io
36 | import os
37 | import sys
38 | import typing as t
39 | from dataclasses import dataclass
40 | from pathlib import Path
41 |
42 | from docspec import Argument, Module
43 | from nr.util.fs import recurse_directory
44 |
45 | from .parser import Parser, ParserOptions
46 |
47 |
48 | def load_python_modules(
49 | modules: t.Optional[t.Sequence[str]] = None,
50 | packages: t.Optional[t.Sequence[str]] = None,
51 | search_path: t.Optional[t.Sequence[t.Union[str, Path]]] = None,
52 | options: t.Optional[ParserOptions] = None,
53 | raise_: bool = True,
54 | encoding: t.Optional[str] = None,
55 | *,
56 | files: t.Optional[t.Sequence[t.Tuple[str, str]]] = None,
57 | ) -> t.Iterable[Module]:
58 | """
59 | Utility function for loading multiple #Module#s from a list of Python module and package
60 | names. It combines #find_module(), #iter_package_files() and #parse_python_module() in a
61 | convenient way.
62 |
63 | # Arguments
64 | modules: A list of module names to load and parse.
65 | packages: A list of package names to load and parse.
66 | search_path: The Python module search path. Falls back to #sys.path if omitted.
67 | options: Options for the Python module parser.
68 | files: A list of `(module_name, filename)` tuples to parse.
69 |
70 | # Returns
71 | Iterable of #Module.
72 | """
73 |
74 | files = list(files or [])
75 | module_name: t.Optional[str]
76 | for module_name in modules or []:
77 | try:
78 | files.append((module_name, find_module(module_name, search_path)))
79 | except ImportError:
80 | if raise_:
81 | raise
82 | for package_name in packages or []:
83 | try:
84 | files.extend(iter_package_files(package_name, search_path))
85 | except ImportError:
86 | if raise_:
87 | raise
88 |
89 | for module_name, filename in files:
90 | yield parse_python_module(filename, module_name=module_name, options=options, encoding=encoding)
91 |
92 |
93 | @t.overload
94 | def parse_python_module(
95 | filename: t.Union[str, Path],
96 | module_name: t.Optional[str] = None,
97 | options: t.Optional[ParserOptions] = None,
98 | encoding: t.Optional[str] = None,
99 | ) -> Module: ...
100 |
101 |
102 | @t.overload
103 | def parse_python_module(
104 | fp: t.TextIO,
105 | filename: t.Union[str, Path],
106 | module_name: t.Optional[str] = None,
107 | options: t.Optional[ParserOptions] = None,
108 | encoding: t.Optional[str] = None,
109 | ) -> Module: ...
110 |
111 |
112 | def parse_python_module( # type: ignore
113 | fp: t.Union[str, Path, t.TextIO],
114 | filename: t.Union[str, Path, None] = None,
115 | module_name: t.Optional[str] = None,
116 | options: t.Optional[ParserOptions] = None,
117 | encoding: t.Optional[str] = None,
118 | ) -> Module:
119 | """
120 | Parses Python code of a file or file-like object and returns a #Module
121 | object with the contents of the file The *options* are forwarded to the
122 | #Parser constructor.
123 | """
124 |
125 | if isinstance(fp, (str, Path)):
126 | if filename:
127 | raise TypeError('"fp" and "filename" both provided, and "fp" is a string/path')
128 | # TODO(NiklasRosenstein): If the file header contains a # coding: comment, we should
129 | # use that instead of the specified or system default encoding.
130 | with io.open(fp, encoding=encoding) as fpobj:
131 | return parse_python_module(fpobj, fp, module_name, options, encoding)
132 |
133 | assert filename is not None
134 | parser = Parser(options)
135 | ast = parser.parse_to_ast(fp.read(), str(filename))
136 | return parser.parse(ast, str(filename), module_name)
137 |
138 |
139 | def find_module(module_name: str, search_path: t.Optional[t.Sequence[t.Union[str, Path]]] = None) -> str:
140 | """Finds the filename of a module that can be parsed with #parse_python_module(). If *search_path* is not set,
141 | the default #sys.path is used to search for the module. If *module_name* is a Python package, it will return the
142 | path to the package's `__init__.py` file. If the module does not exist, an #ImportError is raised. This is also
143 | true for PEP 420 namespace packages that do not provide an `__init__.py` file.
144 |
145 | :raise ImportError: If the module cannot be found.
146 | """
147 |
148 | # NOTE(NiklasRosenstein): We cannot use #pkgutil.find_loader(), #importlib.find_loader()
149 | # or #importlib.util.find_spec() as they weill prefer returning the module that is already
150 | # loaded in #sys.module even if that instance would not be in the specified search_path.
151 |
152 | if search_path is None:
153 | search_path = sys.path
154 |
155 | filenames = [
156 | os.path.join(os.path.join(*module_name.split(".")), "__init__.py"),
157 | os.path.join(*module_name.split(".")) + ".py",
158 | ]
159 |
160 | for path in search_path:
161 | for choice in filenames:
162 | abs_path = os.path.normpath(os.path.join(path, choice))
163 | if os.path.isfile(abs_path):
164 | return abs_path
165 |
166 | raise ImportError(module_name)
167 |
168 |
169 | def iter_package_files(
170 | package_name: str,
171 | search_path: t.Optional[t.Sequence[t.Union[str, Path]]] = None,
172 | ) -> t.Iterable[t.Tuple[str, str]]:
173 | """Returns an iterator for the Python source files in the specified package. The items returned
174 | by the iterator are tuples of the module name and filename. Supports a PEP 420 namespace package
175 | if at least one matching directory with at least one Python source file in it is found.
176 | """
177 |
178 | encountered: t.Set[str] = set()
179 |
180 | try:
181 | package_entrypoint = find_module(package_name, search_path)
182 | encountered.add(package_name)
183 | yield package_name, package_entrypoint
184 | except ImportError:
185 | package_entrypoint = None
186 |
187 | # Find files matching the package name, compatible with PEP 420 namespace packages.
188 | for path in sys.path if search_path is None else search_path:
189 | parent_dir = Path(path, *package_name.split("."))
190 | if not parent_dir.is_dir():
191 | continue
192 | for item in recurse_directory(parent_dir):
193 | if item.suffix == ".py":
194 | parts = item.with_suffix("").relative_to(parent_dir).parts
195 | if parts[-1] == "__init__":
196 | parts = parts[:-1]
197 | module_name = ".".join((package_name,) + parts)
198 | if module_name not in encountered:
199 | encountered.add(module_name)
200 | yield module_name, str(item)
201 |
202 |
203 | @dataclass
204 | class DiscoveryResult:
205 | name: str
206 | Module: t.ClassVar[t.Type["_Module"]]
207 | Package: t.ClassVar[t.Type["_Package"]]
208 |
209 |
210 | @dataclass
211 | class _Module(DiscoveryResult):
212 | filename: str
213 |
214 |
215 | @dataclass
216 | class _Package(DiscoveryResult):
217 | directory: str
218 |
219 |
220 | DiscoveryResult.Module = _Module
221 | DiscoveryResult.Package = _Package
222 |
223 |
224 | def discover(directory: t.Union[str, Path]) -> t.Iterable[DiscoveryResult]:
225 | """
226 | Discovers Python modules and packages in the specified *directory*. The returned generated
227 | returns tuples where the first element of the tuple is the type (either `'module'` or
228 | `'package'`), the second is the name and the third is the path. In case of a package,
229 | the path points to the directory.
230 |
231 | :raises OSError: Propagated from #os.listdir().
232 | """
233 |
234 | # TODO (@NiklasRosenstein): Introspect the contents of __init__.py files to determine
235 | # if we're looking at a namespace package. If we do, continue recursively.
236 |
237 | for name in os.listdir(directory):
238 | if name.endswith(".py") and name.count(".") == 1:
239 | yield DiscoveryResult.Module(name[:-3], os.path.join(directory, name))
240 | else:
241 | full_path = os.path.join(directory, name, "__init__.py")
242 | if os.path.isfile(full_path):
243 | yield DiscoveryResult.Package(name, os.path.join(directory, name))
244 |
245 |
246 | def format_arglist(args: t.Sequence[Argument], render_type_hints: bool = True) -> str:
247 | """
248 | Formats a Python argument list.
249 | """
250 |
251 | result: t.List[str] = []
252 |
253 | for arg in args:
254 | parts = []
255 | if arg.type == Argument.Type.KEYWORD_ONLY and not any(x.startswith("*") for x in result):
256 | result.append("*")
257 | parts = [arg.name]
258 | if arg.datatype and render_type_hints:
259 | parts.append(": " + arg.datatype)
260 | if arg.default_value:
261 | if arg.datatype:
262 | parts.append(" ")
263 | parts.append("=")
264 | if arg.default_value:
265 | if arg.datatype:
266 | parts.append(" ")
267 | parts.append(arg.default_value)
268 | if arg.type == Argument.Type.POSITIONAL_REMAINDER:
269 | parts.insert(0, "*")
270 | elif arg.type == Argument.Type.KEYWORD_REMAINDER:
271 | parts.insert(0, "**")
272 | result.append("".join(parts))
273 |
274 | return ", ".join(result)
275 |
--------------------------------------------------------------------------------
/docspec-python/src/docspec_python/__main__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf8 -*-
2 | # Copyright (c) 2020 Niklas Rosenstein
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to
6 | # deal in the Software without restriction, including without limitation the
7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8 | # sell copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in
12 | # all copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20 | # IN THE SOFTWARE.
21 |
22 | import argparse
23 | import sys
24 |
25 | import docspec
26 |
27 | from docspec_python import DiscoveryResult, ParserOptions, discover, load_python_modules
28 |
29 |
30 | def main() -> None:
31 | parser = argparse.ArgumentParser(
32 | formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=34, width=100),
33 | )
34 | group = parser.add_argument_group("input options")
35 | group.add_argument("-m", "--module", action="append", metavar="MODULE", help="parse the specified module.")
36 | group.add_argument(
37 | "-p", "--package", action="append", metavar="MODULE", help="parse the specified module and submodules."
38 | )
39 | group.add_argument(
40 | "-I",
41 | "--search-path",
42 | metavar="PATH",
43 | action="append",
44 | help="override the module search path. defaults to sys.path.",
45 | )
46 | group.add_argument("-D", "--discover", action="store_true", help="discover available packages in the search path.")
47 | group.add_argument("-E", "--exclude", action="append", help="exclude modules/packages when using --discover.")
48 | group = parser.add_argument_group("parsing options")
49 | group.add_argument("-2", "--python2", action="store_true", help="parse as python 2 source.")
50 | group.add_argument(
51 | "--treat-singleline-comment-blocks-as-docstrings",
52 | action="store_true",
53 | help="parse blocks of single-line comments as docstrings for modules, classes and functions.",
54 | )
55 | group = parser.add_argument_group("output options")
56 | group.add_argument("-l", "--list", action="store_true", help="list modules from the input.")
57 | args = parser.parse_args()
58 |
59 | args.module = args.module or []
60 | args.package = args.package or []
61 |
62 | if args.discover:
63 | for path in args.search_path or sys.path:
64 | try:
65 | discovered_items = list(discover(path))
66 | except FileNotFoundError:
67 | continue
68 | for item in discovered_items:
69 | if args.exclude and item.name in args.exclude:
70 | continue
71 | if isinstance(item, DiscoveryResult.Module):
72 | args.module.append(item.name)
73 | elif isinstance(item, DiscoveryResult.Package):
74 | args.package.append(item.name)
75 | else:
76 | raise RuntimeError(item)
77 |
78 | if not args.module and not args.package:
79 | parser.print_usage()
80 | sys.exit(1)
81 |
82 | options = ParserOptions(
83 | print_function=not args.python2,
84 | treat_singleline_comment_blocks_as_docstrings=args.treat_singleline_comment_blocks_as_docstrings,
85 | )
86 | modules = load_python_modules(args.module, args.package, args.search_path, options)
87 |
88 | if args.list:
89 | for module in sorted(modules, key=lambda x: x.name):
90 | print("| " * module.name.count(".") + module.name.rpartition(".")[-1])
91 | return
92 |
93 | for module in modules:
94 | docspec.dump_module(module, sys.stdout)
95 |
96 |
97 | if __name__ == "__main__":
98 | main()
99 |
--------------------------------------------------------------------------------
/docspec-python/src/docspec_python/parser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf8 -*-
2 | # Copyright (c) 2020 Niklas Rosenstein
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to
6 | # deal in the Software without restriction, including without limitation the
7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8 | # sell copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in
12 | # all copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20 | # IN THE SOFTWARE.
21 |
22 | """
23 | Note: The `docspec_python.parser` module is not public API.
24 | """
25 |
26 | from __future__ import annotations
27 |
28 | import ast
29 | import dataclasses
30 | import logging
31 | import os
32 | import re
33 | import sys
34 | import textwrap
35 | import typing as t
36 | from io import StringIO
37 |
38 | import blib2to3.pgen2.parse
39 | from black.parsing import lib2to3_parse
40 | from blib2to3.pgen2 import token
41 | from blib2to3.pygram import python_symbols as syms
42 | from blib2to3.pytree import NL, Context, Leaf, Node, type_repr
43 | from docspec import (
44 | Argument,
45 | Class,
46 | Decoration,
47 | Docstring,
48 | Function,
49 | Indirection,
50 | Location,
51 | Module,
52 | Variable,
53 | _ModuleMembers,
54 | )
55 | from nr.util.iter import SequenceWalker
56 |
57 | #: Logger for debugging. Slap it in when and where needed.
58 | #:
59 | #: Note to self and others, you can get debug log output with something like
60 | #:
61 | #: do
62 | #: name: "debug-logging"
63 | #: closure: {
64 | #: precedes "copy-files"
65 | #: }
66 | #: action: {
67 | #: logging.getLogger("").setLevel(logging.DEBUG)
68 | #: }
69 | #:
70 | #: in your `build.novella` file. Be warned, it's a _lot_ of output, and lags the
71 | #: build out considerably.
72 | #:
73 | _LOG = logging.getLogger(__name__)
74 |
75 |
76 | class ParseError(blib2to3.pgen2.parse.ParseError): # type: ignore[misc] # Cannot subclass "ParseError" (has type "Any") # noqa: E501
77 | """Extends `blib2to3.pgen2.parse.ParseError` to add a `filename` attribute."""
78 |
79 | msg: t.Text
80 | type: t.Optional[int]
81 | value: t.Optional[t.Text]
82 | context: Context
83 | filename: t.Text
84 |
85 | def __init__(
86 | self, msg: t.Text, type: t.Optional[int], value: t.Optional[t.Text], context: Context, filename: t.Text
87 | ) -> None:
88 | Exception.__init__(
89 | self, "%s: type=%r, value=%r, context=%r, filename=%r" % (msg, type, value, context, filename)
90 | )
91 | self.msg = msg
92 | self.type = type
93 | self.value = value
94 | self.context = context
95 | self.filename = filename
96 |
97 |
98 | def dedent_docstring(s: str) -> str:
99 | lines = s.split("\n")
100 | lines[0] = lines[0].strip()
101 | lines[1:] = textwrap.dedent("\n".join(lines[1:])).split("\n")
102 | return "\n".join(lines).strip()
103 |
104 |
105 | T = t.TypeVar("T")
106 | V = t.TypeVar("V")
107 |
108 |
109 | @t.overload
110 | def find(predicate: t.Callable[[T], t.Any], iterable: t.Iterable[T], as_type: None = None) -> T | None: ...
111 |
112 |
113 | @t.overload
114 | def find(predicate: t.Callable[[T], t.Any], iterable: t.Iterable[T], as_type: type[V]) -> V | None: ...
115 |
116 |
117 | @t.overload
118 | def find(predicate: None, iterable: t.Iterable[T], as_type: type[V]) -> V | None: ...
119 |
120 |
121 | def find(
122 | predicate: t.Callable[[T], t.Any] | None, iterable: t.Iterable[T], as_type: type[V] | None = None
123 | ) -> T | V | None:
124 | """Basic find function, plus the ability to add an `as_type` argument and
125 | receive a typed result (or raise `TypeError`).
126 |
127 | As you might expect, this is really only to make typing easier.
128 | """
129 |
130 | if predicate is None and as_type is not None:
131 | expect = as_type
132 | predicate = lambda x: isinstance(x, expect) # noqa: E731
133 | assert predicate is not None
134 |
135 | for item in iterable:
136 | if predicate(item):
137 | if (as_type is not None) and (not isinstance(item, as_type)):
138 | raise TypeError(
139 | "expected predicate to only match type {}, matched {!r}".format(
140 | as_type,
141 | item,
142 | )
143 | )
144 | return item
145 | return None
146 |
147 |
148 | @t.overload
149 | def get(predicate: t.Callable[[T], object], iterable: t.Iterable[T], as_type: None = None) -> T: ...
150 |
151 |
152 | @t.overload
153 | def get(predicate: t.Callable[[T], object], iterable: t.Iterable[T], as_type: type[V]) -> V: ...
154 |
155 |
156 | def get(predicate: t.Callable[[T], object], iterable: t.Iterable[T], as_type: type[V] | None = None) -> T | V:
157 | """Like `find`, but raises `ValueError` if `predicate` does not match. Assumes
158 | that `None` means "no match", so don't try to use it to get `None` values in
159 | `iterable`.
160 | """
161 |
162 | found = find(predicate, iterable, as_type)
163 | if found is None:
164 | raise ValueError("item not found for predicate {!r} in iterable {!r}".format(predicate, iterable))
165 |
166 | return found
167 |
168 |
169 | def get_type_name(nl: NL) -> str:
170 | """Get the "type name" for a `blib2to3.pytree.NL`, which is a `Node` or
171 | `Leaf`. For display / debugging purposes.
172 | """
173 | if isinstance(nl, Node):
174 | return str(type_repr(nl.type))
175 | return str(token.tok_name.get(nl.type, nl.type))
176 |
177 |
178 | def pprint_nl(nl: NL, file: t.IO[str] = sys.stdout, indent: int = 4, _depth: int = 0) -> None:
179 | """Pretty-print a `blib2to3.pytree.NL` over a bunch of lines, with indents,
180 | to make it easier to read. Display / debugging use.
181 | """
182 | assert nl.type is not None
183 |
184 | indent_s = " " * indent * _depth
185 |
186 | if nl.children:
187 | print(
188 | "{indent_s}{class_name}({type_name}, [".format(
189 | indent_s=indent_s,
190 | class_name=nl.__class__.__name__,
191 | type_name=get_type_name(nl),
192 | ),
193 | file=file,
194 | )
195 | for child in nl.children:
196 | pprint_nl(child, file=file, _depth=_depth + 1)
197 | print("{indent_s}])".format(indent_s=indent_s), file=file)
198 | else:
199 | print(
200 | "{indent_s}{class_name}({type_name}, [])".format(
201 | indent_s=indent_s,
202 | class_name=nl.__class__.__name__,
203 | type_name=get_type_name(nl),
204 | ),
205 | file=file,
206 | )
207 |
208 |
209 | def pformat_nl(nl: NL) -> str:
210 | """Same as `pprint_nl`, but writes to a `str`."""
211 | sio = StringIO()
212 | pprint_nl(nl, file=sio)
213 | return sio.getvalue()
214 |
215 |
216 | def get_value(node: NL) -> str:
217 | if isinstance(node, Leaf):
218 | return t.cast(str, node.value)
219 | raise TypeError("expected node to have a `value` attribute (be a Leaf), given {!r}".format(node))
220 |
221 |
222 | @dataclasses.dataclass
223 | class ParserOptions:
224 | # NOTE (@nrser) This is no longer used. It was passed to
225 | # `lib2to3.refactor.RefactoringTool`, but that's been swapped out for
226 | # `black.parsing.lib2to3_parse`, which does not take the same options.
227 | #
228 | # It looks like it supported Python 2.x code, and I don't see anything
229 | # before 3.3 in `black.mode.TargetVersion`, so 2.x might be completely off
230 | # the table when using the Black parser.
231 | print_function: bool = True
232 | treat_singleline_comment_blocks_as_docstrings: bool = False
233 |
234 |
235 | class Parser:
236 | def __init__(self, options: t.Optional[ParserOptions] = None) -> None:
237 | self.options = options or ParserOptions()
238 |
239 | def parse_to_ast(self, code: str, filename: str) -> NL:
240 | """
241 | Parses the string *code* to an AST with #lib2to3.
242 | """
243 |
244 | try:
245 | # NOTE (@NiklasRosenstein): Adding newline at the end, a ParseError
246 | # could be raised without a trailing newline (tested in CPython 3.6
247 | # and 3.7).
248 | return lib2to3_parse(code + "\n")
249 | except ParseError as exc:
250 | raise ParseError(exc.msg, exc.type, exc.value, exc.context, filename)
251 |
252 | def parse(self, ast: NL, filename: str, module_name: str | None = None) -> Module:
253 | self.filename = filename # pylint: disable=attribute-defined-outside-init
254 |
255 | if module_name is None:
256 | module_name = os.path.basename(filename)
257 | module_name = os.path.splitext(module_name)[0]
258 | if module_name == "__init__":
259 | module_name = os.path.basename(os.path.dirname(filename))
260 |
261 | docstring = self.get_docstring_from_first_node(ast, module_level=True)
262 | module = Module(
263 | name=module_name,
264 | location=self.location_from(ast),
265 | docstring=docstring,
266 | members=[],
267 | )
268 |
269 | for node in ast.children:
270 | member = self.parse_declaration(module, node)
271 | if isinstance(member, list):
272 | module.members += member
273 | elif member:
274 | module.members.append(member)
275 |
276 | module.sync_hierarchy()
277 | return module
278 |
279 | def parse_declaration(
280 | self, parent: NL, node: NL, decorations: t.Optional[list[Decoration]] = None
281 | ) -> t.Union[None, _ModuleMembers, t.List[_ModuleMembers]]:
282 | if node.type == syms.simple_stmt:
283 | assert not decorations
284 | stmt = node.children[0]
285 | if stmt.type in (
286 | syms.import_stmt,
287 | syms.import_name,
288 | syms.import_from,
289 | syms.import_as_names,
290 | syms.import_as_name,
291 | ):
292 | return list(self.parse_import(node, stmt))
293 | elif stmt.type == syms.expr_stmt:
294 | return self.parse_statement(node, stmt)
295 | elif node.type == syms.funcdef:
296 | return self.parse_funcdef(parent, node, False, decorations)
297 | elif node.type == syms.classdef:
298 | return self.parse_classdef(parent, node, decorations)
299 | elif node.type in (syms.async_stmt, syms.async_funcdef):
300 | child = node.children[1]
301 | if child.type == syms.funcdef:
302 | return self.parse_funcdef(parent, child, True, decorations)
303 | elif node.type == syms.decorated:
304 | assert len(node.children) == 2
305 | decorations = []
306 | if node.children[0].type == syms.decorator:
307 | decorator_nodes = [node.children[0]]
308 | elif node.children[0].type == syms.decorators:
309 | decorator_nodes = node.children[0].children
310 | else:
311 | assert False, node.children[0].type
312 | for child in decorator_nodes:
313 | assert child.type == syms.decorator, child.type
314 | decorations.append(self.parse_decorator(child))
315 | return self.parse_declaration(parent, node.children[1], decorations)
316 | return None
317 |
318 | def _split_statement(self, stmt: Node) -> tuple[list[NL], list[NL], list[NL]]:
319 | """
320 | Parses a statement node into three lists, consisting of the leaf nodes
321 | that are the name(s), annotation and value of the expression. The value
322 | list will be empty if this is not an assignment statement (but for example
323 | a plain expression).
324 | """
325 |
326 | def _parse(
327 | stack: list[tuple[str, list[NL]]], current: tuple[str, list[NL]], stmt: Node
328 | ) -> list[tuple[str, list[NL]]]:
329 | for child in stmt.children:
330 | if not isinstance(child, Node) and child.value == "=":
331 | stack.append(current)
332 | current = ("value", [])
333 | elif not isinstance(child, Node) and child.value == ":":
334 | stack.append(current)
335 | current = ("annotation", [])
336 | elif isinstance(child, Node) and child.type == getattr(syms, "annassign", None): # >= 3.6
337 | _parse(stack, current, child)
338 | else:
339 | current[1].append(child)
340 | stack.append(current)
341 | return stack
342 |
343 | result: dict[str, list[NL]] = dict(_parse([], ("names", []), stmt))
344 | return result.get("names", []), result.get("annotation", []), result.get("value", [])
345 |
346 | def parse_import(self, parent: NL, node: Node) -> t.Iterable[Indirection]:
347 | def _single_import_to_indirection(node: t.Union[Node, Leaf]) -> Indirection:
348 | if node.type == syms.dotted_as_name: # example: urllib.request as r
349 | target = self.name_to_string(node.children[0])
350 | name = self.name_to_string(node.children[2])
351 | return Indirection(self.location_from(node), name, None, target)
352 | elif node.type == syms.dotted_name: # example os.path
353 | name = self.name_to_string(node)
354 | return Indirection(self.location_from(node), name.split(".")[-1], None, name)
355 | elif isinstance(node, Leaf):
356 | return Indirection(self.location_from(node), node.value, None, node.value)
357 | else:
358 | raise RuntimeError(f"cannot handle {node!r}")
359 |
360 | def _from_import_to_indirection(prefix: str, node: t.Union[Node, Leaf]) -> Indirection:
361 | if node.type == syms.import_as_name: # example: Widget as W
362 | target = self.name_to_string(node.children[0])
363 | name = self.name_to_string(node.children[2])
364 | return Indirection(self.location_from(node), name, None, prefix + "." + target)
365 | elif isinstance(node, Leaf): # example: Widget
366 | name = self.name_to_string(node)
367 | if not prefix.endswith("."):
368 | prefix += "."
369 | return Indirection(self.location_from(node), name, None, prefix + name)
370 | else:
371 | raise RuntimeError(f"cannot handle {node!r}")
372 |
373 | if node.type == syms.import_name: # example: import ...
374 | subject_node = node.children[1]
375 | if subject_node.type == syms.dotted_as_names:
376 | yield from (_single_import_to_indirection(n) for n in subject_node.children if n.type != token.COMMA)
377 | else:
378 | yield _single_import_to_indirection(subject_node)
379 |
380 | elif node.type == syms.import_from: # example: from xyz import ...
381 | index = next(
382 | i
383 | for i, n in enumerate(node.children)
384 | if isinstance(n, Leaf) and n.type == token.NAME and n.value == "import"
385 | )
386 | name = "".join(self.name_to_string(x) for x in node.children[1:index])
387 | subject_node = node.children[index + 1]
388 | if subject_node.type == token.LPAR:
389 | subject_node = node.children[index + 2]
390 | if subject_node.type == syms.import_as_names:
391 | yield from (
392 | _from_import_to_indirection(name, n)
393 | for n in subject_node.children
394 | if n.type not in (token.LPAR, token.RPAR, token.COMMA)
395 | )
396 | else:
397 | yield _from_import_to_indirection(name, subject_node)
398 |
399 | else:
400 | raise RuntimeError(f"dont know how to deal with {node!r}")
401 |
402 | def parse_statement(self, parent: Node, stmt: Node) -> t.Optional[Variable]:
403 | names, annotation, value = self._split_statement(stmt)
404 | data: t.Optional[Variable] = None
405 | if value or annotation:
406 | docstring = self.get_statement_docstring(stmt)
407 | expr = self.nodes_to_string(value) if value else None
408 | annotation_as_string = self.nodes_to_string(annotation) if annotation else None
409 | assert names and len(names) == 1, (stmt, names)
410 |
411 | # NOTE (@NiklasRosenstein): `names` here may be a Leaf(NAME) node if we only got a
412 | # single variable on the left, or a Node(testlist_star_expr) if the left operand
413 | # is a more complex tuple- or range-unpacking.
414 | #
415 | # We don't support multiple assignments in Docspec as we cannot tell how an associated
416 | # docstring should be assigned to each of the resulting Variable()s, nor how the right
417 | # side of the expression should be distributed among them.
418 | if names[0].type != token.NAME:
419 | return None
420 |
421 | # The parent node probably ends with a Leaf(NEWLINE), which will have, as its prefix, the
422 | # comment on the remainder of the line. Any docstring we found before or after the declaration
423 | # however takes precedence.
424 | if not docstring:
425 | docstring = self.prepare_docstring(parent.children[-1].prefix, parent.children[-1])
426 |
427 | name = self.nodes_to_string(names)
428 | data = Variable(
429 | name=name,
430 | location=self.location_from(stmt),
431 | docstring=docstring,
432 | datatype=annotation_as_string,
433 | value=expr,
434 | )
435 |
436 | return data
437 |
438 | def parse_decorator(self, node: Node) -> Decoration:
439 | assert get_value(node.children[0]) == "@"
440 |
441 | # NOTE (@nrser)I have no idea why `blib2to3` parses some decorators with a 'power'
442 | # node (which _seems_ refer to the exponent operator `**`), but it
443 | # does.
444 | #
445 | # The hint I eventually found was:
446 | #
447 | # https://github.com/psf/black/blob/b0d1fba7ac3be53c71fb0d3211d911e629f8aecb/src/black/nodes.py#L657
448 | #
449 | # Anyways, this works around that curiosity.
450 | if node.children[1].type == syms.power:
451 | name = self.name_to_string(node.children[1].children[0])
452 | call_expr = self.nodes_to_string(node.children[1].children[1:]).strip()
453 |
454 | else:
455 | name = self.name_to_string(node.children[1])
456 | call_expr = self.nodes_to_string(node.children[2:]).strip()
457 |
458 | return Decoration(location=self.location_from(node), name=name, args=call_expr or None)
459 |
460 | def parse_funcdef(
461 | self, parent: Node, node: Node, is_async: bool, decorations: t.Optional[list[Decoration]]
462 | ) -> Function:
463 | parameters = get(lambda x: x.type == syms.parameters, node.children, as_type=Node)
464 | body = find(lambda x: x.type == syms.suite, node.children, as_type=Node) or get(
465 | lambda x: x.type == syms.simple_stmt, node.children, as_type=Node
466 | )
467 |
468 | name = get_value(node.children[1])
469 | docstring = self.get_docstring_from_first_node(body)
470 | args = self.parse_parameters(parameters)
471 | return_ = self.get_return_annotation(node)
472 | decorations = decorations or []
473 |
474 | return Function(
475 | name=name,
476 | location=self.location_from(node),
477 | docstring=docstring,
478 | modifiers=["async"] if is_async else None,
479 | args=args,
480 | return_type=return_,
481 | decorations=decorations,
482 | )
483 |
484 | def parse_argument(
485 | self,
486 | node: t.Optional[NL],
487 | argtype: Argument.Type,
488 | scanner: SequenceWalker[NL],
489 | ) -> Argument:
490 | """
491 | Parses an argument from the AST. *node* must be the current node at
492 | the current position of the *scanner*. The scanner is used to extract
493 | the optional default argument value that follows the *node*.
494 | """
495 |
496 | def parse_annotated_name(node: NL) -> tuple[str, t.Optional[str]]:
497 | if node.type in (syms.tname, syms.tname_star):
498 | scanner = SequenceWalker(node.children)
499 | name = get_value(scanner.current)
500 | node = scanner.next()
501 | assert node.type == token.COLON, node.parent
502 | node = scanner.next()
503 | annotation = self.nodes_to_string([node])
504 | elif node:
505 | name = get_value(node)
506 | annotation = None
507 | else:
508 | raise RuntimeError("unexpected node: {!r}".format(node))
509 | return (name, annotation)
510 |
511 | assert node is not None
512 | location = self.location_from(node)
513 | name, annotation = parse_annotated_name(node)
514 | assert name not in "/*", repr(node)
515 |
516 | node = scanner.advance()
517 | default = None
518 | if node and node.type == token.EQUAL:
519 | node = scanner.advance()
520 | assert node is not None
521 | default = self.nodes_to_string([node])
522 | scanner.advance()
523 |
524 | return Argument(
525 | location=location,
526 | name=name,
527 | type=argtype,
528 | datatype=annotation,
529 | default_value=default,
530 | )
531 |
532 | def parse_parameters(self, parameters: Node) -> list[Argument]:
533 | assert parameters.type == syms.parameters, parameters.type
534 | result: t.List[Argument] = []
535 |
536 | arglist = find(lambda x: x.type == syms.typedargslist, parameters.children)
537 | if not arglist:
538 | # NOTE (@NiklasRosenstein): A single argument (annotated or not) does
539 | # not get wrapped in a `typedargslist`, but in a single `tname`.
540 | tname = find(lambda x: x.type == syms.tname, parameters.children)
541 | if tname:
542 | scanner = SequenceWalker(parameters.children, parameters.children.index(tname))
543 | result.append(self.parse_argument(tname, Argument.Type.POSITIONAL, scanner))
544 | else:
545 | # This must be either ["(", ")"] or ["(", "argname", ")"].
546 | assert len(parameters.children) in (2, 3), parameters.children
547 | if len(parameters.children) == 3:
548 | result.append(
549 | Argument(
550 | location=self.location_from(parameters.children[1]),
551 | name=get_value(parameters.children[1]),
552 | type=Argument.Type.POSITIONAL,
553 | decorations=None,
554 | datatype=None,
555 | default_value=None,
556 | )
557 | )
558 | return result
559 |
560 | argtype = Argument.Type.POSITIONAL
561 |
562 | index = SequenceWalker(arglist.children)
563 | for node in index.safe_iter():
564 | node = index.current
565 |
566 | if node.type == token.SLASH:
567 | assert argtype == Argument.Type.POSITIONAL
568 | # We need to retrospectively change the argument type of previous parsed arguments to POSITIONAL_ONLY.
569 | for arg in result:
570 | assert arg.type == Argument.Type.POSITIONAL, arg
571 | arg.type = Argument.Type.POSITIONAL_ONLY
572 | # There may not be another token after the '/' -- seems like it totally
573 | # works to define a function like
574 | #
575 | # def f(x, y, /):
576 | # ...
577 | #
578 | node = index.advance()
579 | if node is not None and node.type == token.COMMA:
580 | index.advance()
581 |
582 | elif node.type == token.STAR:
583 | node = index.next()
584 | if node and node.type != token.COMMA:
585 | result.append(self.parse_argument(node, Argument.Type.POSITIONAL_REMAINDER, index))
586 | index.advance()
587 | argtype = Argument.Type.KEYWORD_ONLY
588 |
589 | elif node and node.type == token.DOUBLESTAR:
590 | node = index.next()
591 | result.append(self.parse_argument(node, Argument.Type.KEYWORD_REMAINDER, index))
592 | argtype = Argument.Type.KEYWORD_ONLY
593 | index.advance()
594 |
595 | else:
596 | result.append(self.parse_argument(node, argtype, index))
597 | index.advance()
598 |
599 | return result
600 |
601 | def parse_classdef_arglist(self, classargs: NL) -> tuple[str | None, list[str]]:
602 | metaclass = None
603 | bases = []
604 | for child in classargs.children[::2]:
605 | if child.type == syms.argument:
606 | key, value = child.children[0].value, self.nodes_to_string(child.children[2:])
607 | if key == "metaclass":
608 | metaclass = value
609 | else:
610 | # TODO @NiklasRosenstein: handle metaclass arguments
611 | pass
612 | else:
613 | bases.append(str(child))
614 | return metaclass, bases
615 |
616 | def parse_classdef_rawargs(self, classdef: NL) -> tuple[str | None, list[str]]:
617 | metaclass = None
618 | bases = []
619 | index = SequenceWalker(classdef.children, 2)
620 | if index.current.type == token.LPAR:
621 | index.next()
622 | while index.current.type != token.RPAR:
623 | if index.current.type == syms.argument:
624 | key = index.current.children[0].value
625 | value = str(index.current.children[2])
626 | if key == "metaclass":
627 | metaclass = value
628 | else:
629 | # TODO @NiklasRosenstein: handle metaclass arguments
630 | pass
631 | else:
632 | bases.append(str(index.current))
633 | index.next()
634 | return metaclass, bases
635 |
636 | def parse_classdef(self, parent: Node, node: Node, decorations: t.Optional[list[Decoration]]) -> Class:
637 | name = get_value(node.children[1])
638 | bases: list[str] = []
639 | metaclass = None
640 |
641 | # An arglist is available if there are at least two parameters.
642 | # Otherwise we have to deal with parsing a raw sequence of nodes.
643 | classargs = find(lambda x: x.type == syms.arglist, node.children)
644 | if classargs:
645 | metaclass, bases = self.parse_classdef_arglist(classargs)
646 | else:
647 | metaclass, bases = self.parse_classdef_rawargs(node)
648 |
649 | suite = find(lambda x: x.type == syms.suite, node.children)
650 | docstring = self.get_docstring_from_first_node(suite) if suite else None
651 | class_ = Class(
652 | name=name,
653 | location=self.location_from(node),
654 | docstring=docstring,
655 | metaclass=metaclass,
656 | bases=[b.strip() for b in bases],
657 | decorations=decorations,
658 | members=[],
659 | )
660 |
661 | for child in suite.children if suite else []:
662 | if isinstance(child, Node):
663 | members = self.parse_declaration(class_, child) or []
664 | if not isinstance(members, list):
665 | members = [members]
666 | for member in members:
667 | assert not isinstance(member, Module)
668 | if metaclass is None and isinstance(member, Variable) and member.name == "__metaclass__":
669 | metaclass = member.value
670 | elif member:
671 | class_.members.append(member)
672 |
673 | class_.metaclass = metaclass
674 | return class_
675 |
676 | def location_from(self, node: NL) -> Location:
677 | # NOTE (@nrser) `blib2to3.pytree.Base.get_lineno` may return `None`, but
678 | # `Location` expects an `int`, so not sure exactly what to do here... for
679 | # the moment just return a bogus value of -1
680 | lineno = node.get_lineno()
681 | if lineno is None:
682 | lineno = -1
683 | return Location(self.filename, lineno)
684 |
685 | def get_return_annotation(self, node: Node) -> t.Optional[str]:
686 | rarrow = find(lambda x: x.type == token.RARROW, node.children)
687 | if rarrow:
688 | assert rarrow.next_sibling # satisfy type checker
689 | return self.nodes_to_string([rarrow.next_sibling])
690 | return None
691 |
692 | def get_most_recent_prefix(self, node: NL) -> str:
693 | if node.prefix:
694 | return t.cast(str, node.prefix)
695 | while not node.prev_sibling and not node.prefix:
696 | if not node.parent:
697 | return ""
698 | node = node.parent
699 | if node.prefix:
700 | return t.cast(str, node.prefix)
701 | while isinstance(node.prev_sibling, Node) and node.prev_sibling.children:
702 | node = node.prev_sibling.children[-1]
703 | return t.cast(str, node.prefix)
704 |
705 | def get_docstring_from_first_node(self, parent: NL, module_level: bool = False) -> t.Optional[Docstring]:
706 | """
707 | This method retrieves the docstring for the block node *parent*. The
708 | node either declares a class or function.
709 | """
710 |
711 | assert parent is not None
712 | node = find(None, parent.children, as_type=Node)
713 |
714 | if node and node.type == syms.simple_stmt and node.children[0].type == token.STRING:
715 | return self.prepare_docstring(get_value(node.children[0]), parent)
716 |
717 | if not node and not module_level:
718 | return None
719 |
720 | if self.options.treat_singleline_comment_blocks_as_docstrings:
721 | docstring, doc_type = self.get_hashtag_docstring_from_prefix(node or parent)
722 | if doc_type == "block":
723 | return docstring
724 |
725 | return None
726 |
727 | def get_statement_docstring(self, node: NL) -> t.Optional[Docstring]:
728 | prefix = self.get_most_recent_prefix(node)
729 | match = re.match(r"\s*", prefix[::-1])
730 | assert match is not None
731 | ws = match.group(0)
732 | if ws.count("\n") == 1:
733 | docstring, doc_type = self.get_hashtag_docstring_from_prefix(node)
734 | if doc_type == "statement":
735 | return docstring
736 | # Look for the next string literal instead.
737 | curr: t.Optional[NL] = node
738 | while curr and curr.type != syms.simple_stmt:
739 | curr = curr.parent
740 | if curr and curr.next_sibling and curr.next_sibling.type == syms.simple_stmt:
741 | string_literal = curr.next_sibling.children[0]
742 | if string_literal.type == token.STRING:
743 | assert isinstance(string_literal, Leaf)
744 | return self.prepare_docstring(string_literal.value, string_literal)
745 | return None
746 |
747 | def get_hashtag_docstring_from_prefix(
748 | self,
749 | node: NL,
750 | ) -> t.Tuple[t.Optional[Docstring], t.Optional[str]]:
751 | """
752 | Given a node in the AST, this method retrieves the docstring from the
753 | closest prefix of this node (ie. any block of single-line comments that
754 | precede this node).
755 |
756 | The function will also return the type of docstring: A docstring that
757 | begins with `#:` is a statement docstring, otherwise it is a block
758 | docstring (and only used for classes/methods).
759 |
760 | return: (docstring, doc_type)
761 | """
762 |
763 | prefix = self.get_most_recent_prefix(node)
764 | lines: t.List[str] = []
765 | doc_type = None
766 | for line in reversed(prefix.split("\n")):
767 | line = line.strip()
768 | if lines and not line.startswith("#"):
769 | break
770 | if doc_type is None and line.strip().startswith("#:"):
771 | doc_type = "statement"
772 | elif doc_type is None and line.strip().startswith("#"):
773 | doc_type = "block"
774 | if lines or line:
775 | lines.append(line)
776 |
777 | return self.prepare_docstring("\n".join(reversed(lines)), node), doc_type
778 |
779 | def prepare_docstring(self, s: str, node_for_location: NL) -> t.Optional[Docstring]:
780 | location = self.location_from(node_for_location)
781 | s = s.strip()
782 | if s.startswith("#"):
783 | location.lineno -= s.count("\n") + 2
784 | lines = []
785 | initial_indent: t.Optional[int] = None
786 | for line in s.split("\n"):
787 | if line.startswith("#:"):
788 | line = line[2:]
789 | elif line.startswith("#"):
790 | line = line[1:]
791 | else:
792 | assert False, repr(line)
793 | if initial_indent is None:
794 | initial_indent = len(line) - len(line.lstrip())
795 | # Strip up to initial_indent whitespace from the line.
796 | new_line = line.lstrip()
797 | new_line = " " * max(0, len(line) - len(new_line) - initial_indent) + new_line
798 | lines.append(new_line.rstrip())
799 | return Docstring(location, "\n".join(lines).strip())
800 | if s:
801 | s = ast.literal_eval(s)
802 | return Docstring(location, dedent_docstring(s).strip())
803 | return None
804 |
805 | def nodes_to_string(self, nodes: list[NL]) -> str:
806 | """
807 | Converts a list of AST nodes to a string.
808 | """
809 |
810 | def generator(nodes: t.List[NL], skip_prefix: bool = True) -> t.Iterable[str]:
811 | for i, node in enumerate(nodes):
812 | if not skip_prefix or i != 0:
813 | yield node.prefix
814 | if isinstance(node, Node):
815 | yield from generator(node.children, True)
816 | else:
817 | yield node.value
818 |
819 | return "".join(generator(nodes))
820 |
821 | def name_to_string(self, node: NL) -> str:
822 | if node.type == syms.dotted_name:
823 | return "".join(get_value(x) for x in node.children)
824 | else:
825 | return get_value(node)
826 |
--------------------------------------------------------------------------------
/docspec-python/src/docspec_python/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NiklasRosenstein/python-docspec/61d3e38c55d290da6197236357e1cdfe818d35b5/docspec-python/src/docspec_python/py.typed
--------------------------------------------------------------------------------
/docspec-python/test/src/pep420_namespace_package/module.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NiklasRosenstein/python-docspec/61d3e38c55d290da6197236357e1cdfe818d35b5/docspec-python/test/src/pep420_namespace_package/module.py
--------------------------------------------------------------------------------
/docspec-python/test/test_loader.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf8 -*-
2 | # Copyright (c) 2020 Niklas Rosenstein
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to
6 | # deal in the Software without restriction, including without limitation the
7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8 | # sell copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in
12 | # all copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20 | # IN THE SOFTWARE.
21 |
22 | """Tests the docspec loading mechanism. Expects that the `docspec` and `docspec_python` modules are installed.
23 | For a full test, they need to be installed as usual (not in develop mode).
24 | """
25 |
26 | import os
27 | import typing as t
28 | from pathlib import Path
29 |
30 | import docspec
31 | import pytest
32 |
33 | import docspec_python
34 |
35 |
36 | def _assert_is_docspec_python_module(modules: t.List[docspec.Module]) -> None:
37 | assert sorted(m.name for m in modules) == ["docspec_python", "docspec_python.__main__", "docspec_python.parser"]
38 |
39 |
40 | def test_discovery_from_sys_path() -> None:
41 | """Tests that the `docspec_python` module can be loaded from `sys.path`."""
42 |
43 | modules = list(docspec_python.load_python_modules(packages=["docspec_python"]))
44 | _assert_is_docspec_python_module(modules)
45 |
46 |
47 | def test_discovery_search_path_overrides() -> None:
48 | """Tests that the `docspec_python` module will not be loaded if an empty search path is supplied."""
49 |
50 | modules = list(docspec_python.load_python_modules(packages=["docspec_python"], search_path=[], raise_=False))
51 | assert not modules
52 |
53 |
54 | @pytest.mark.skipif(
55 | os.getenv("DOCSPEC_TEST_NO_DEVELOP") != "true",
56 | reason='DOCSPEC_TEST_NO_DEVELOP needs to be set to "true" to test this case',
57 | )
58 | def test_discovery_search_path_overrides_docspec_python_in_install_mode() -> None:
59 | """Tests that the `docspec_python` module can be loaded separately from the local project source code as well
60 | as from the system site-packages independently by supplying the right search path."""
61 |
62 | src_dir = os.path.normpath(__file__ + "/../../src")
63 | src_modules = list(docspec_python.load_python_modules(packages=["docspec_python"], search_path=[src_dir]))
64 | _assert_is_docspec_python_module(src_modules)
65 |
66 | # NOTE: Since we moved to UV, it seems to be adding the local project source code to the sys.path, so we
67 | # don't actually import docspec from site-packages. This test is disabled for now.
68 |
69 | # site_modules = list(
70 | # docspec_python.load_python_modules(packages=["docspec_python"], search_path=site.getsitepackages())
71 | # )
72 | # _assert_is_docspec_python_module(site_modules)
73 |
74 | # assert site_modules[0].location.filename != src_modules[0].location.filename
75 |
76 |
77 | def test_pep420_namespace_package() -> None:
78 | """Tests that PEP 420 namespace packages can be loaded."""
79 |
80 | src_dir = Path(__file__).parent / "src"
81 |
82 | # Test that the module can be loaded explicitly.
83 | src_modules = list(
84 | docspec_python.load_python_modules(modules=["pep420_namespace_package.module"], search_path=[src_dir])
85 | )
86 |
87 | assert len(src_modules) == 1
88 | assert src_modules[0].name == "pep420_namespace_package.module"
89 |
90 | # Test that the module can be loaded implicitly.
91 | src_modules = list(docspec_python.load_python_modules(packages=["pep420_namespace_package"], search_path=[src_dir]))
92 |
93 | assert len(src_modules) == 1
94 | assert src_modules[0].name == "pep420_namespace_package.module"
95 |
--------------------------------------------------------------------------------
/docspec-python/test/test_parser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf8 -*-
2 | # Copyright (c) 2020 Niklas Rosenstein
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to
6 | # deal in the Software without restriction, including without limitation the
7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8 | # sell copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in
12 | # all copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20 | # IN THE SOFTWARE.
21 |
22 | from __future__ import annotations
23 |
24 | import sys
25 | from functools import wraps
26 | from io import StringIO
27 | from json import dumps
28 | from textwrap import dedent
29 | from typing import Any, Callable, List, Optional, TypeVar
30 |
31 | import pytest
32 | from docspec import (
33 | ApiObject,
34 | Argument,
35 | Class,
36 | Decoration,
37 | Docstring,
38 | Function,
39 | HasLocation,
40 | HasMembers,
41 | Indirection,
42 | Location,
43 | Module,
44 | Variable,
45 | _ModuleMemberType,
46 | dump_module,
47 | )
48 | from nr.util.inspect import get_callsite
49 |
50 | from docspec_python import ParserOptions, format_arglist, parse_python_module
51 |
52 | T = TypeVar("T")
53 | DocspecTest = Callable[[], List[_ModuleMemberType]]
54 | loc = Location("", 0, None)
55 |
56 |
57 | def mkfunc(
58 | name: str,
59 | docstring: Optional[str],
60 | lineno: int,
61 | args: List[Argument],
62 | modifiers: Optional[List[str]] = None,
63 | return_type: Optional[str] = None,
64 | ) -> Function:
65 | return Function(
66 | name=name,
67 | location=loc,
68 | docstring=Docstring(Location(get_callsite().code_name, lineno), docstring) if docstring else None,
69 | modifiers=modifiers,
70 | args=args,
71 | return_type=return_type,
72 | decorations=[],
73 | )
74 |
75 |
76 | def unset_location(obj: HasLocation) -> None:
77 | if isinstance(obj, HasLocation):
78 | obj.location = loc
79 | if isinstance(obj, ApiObject) and obj.docstring:
80 | unset_location(obj.docstring)
81 | if isinstance(obj, HasMembers):
82 | for member in obj.members:
83 | unset_location(member)
84 | elif isinstance(obj, Function):
85 | for arg in obj.args:
86 | unset_location(arg)
87 |
88 |
89 | def docspec_test(
90 | module_name: str | None = None, parser_options: ParserOptions | None = None, strip_locations: bool = True
91 | ) -> Callable[[DocspecTest], Callable[[], None]]:
92 | """
93 | Decorator for docspec unit tests.
94 | """
95 |
96 | def decorator(func: DocspecTest) -> Callable[[], None]:
97 | @wraps(func)
98 | def wrapper(*args: Any, **kwargs: Any) -> None:
99 | parsed_module = parse_python_module(
100 | StringIO(dedent(func.__doc__ or "")),
101 | module_name=module_name or func.__name__.lstrip("test_"),
102 | options=parser_options,
103 | filename=func.__name__,
104 | )
105 | parsed_module.location = loc
106 | reference_module = Module(
107 | name=parsed_module.name, location=loc, docstring=None, members=func(*args, **kwargs)
108 | )
109 | if strip_locations:
110 | unset_location(parsed_module)
111 | unset_location(reference_module)
112 | assert dumps(dump_module(reference_module), indent=2) == dumps(dump_module(parsed_module), indent=2)
113 |
114 | return wrapper
115 |
116 | return decorator
117 |
118 |
119 | @docspec_test()
120 | def test_funcdef_1() -> List[_ModuleMemberType]:
121 | """
122 | def a():
123 | ' A simple function. '
124 | """
125 |
126 | return [
127 | Function(
128 | name="a",
129 | location=loc,
130 | docstring=Docstring(Location("test_funcdef_1", 2), "A simple function."),
131 | modifiers=None,
132 | args=[],
133 | return_type=None,
134 | decorations=[],
135 | )
136 | ]
137 |
138 |
139 | @docspec_test()
140 | def test_funcdef_2() -> List[_ModuleMemberType]:
141 | """
142 | def b(a: int, *, c: str, **opts: Any) -> None:
143 | ' This uses annotations and keyword-only arguments. '
144 | """
145 |
146 | return [
147 | Function(
148 | name="b",
149 | location=loc,
150 | docstring=Docstring(Location("test_funcdef_2", 2), "This uses annotations and keyword-only arguments."),
151 | modifiers=None,
152 | args=[
153 | Argument(loc, "a", Argument.Type.POSITIONAL, None, "int", None),
154 | Argument(loc, "c", Argument.Type.KEYWORD_ONLY, None, "str", None),
155 | Argument(loc, "opts", Argument.Type.KEYWORD_REMAINDER, None, "Any", None),
156 | ],
157 | return_type="None",
158 | decorations=[],
159 | )
160 | ]
161 |
162 |
163 | @docspec_test()
164 | def test_funcdef_3() -> List[_ModuleMemberType]:
165 | """
166 | @classmethod
167 | @db_session(sql_debug=True)
168 | def c(self, a: int, b, *args, opt: str) -> Optional[int]:
169 | ' More arg variations. '
170 | """
171 |
172 | return [
173 | Function(
174 | name="c",
175 | location=loc,
176 | docstring=Docstring(Location("test_funcdef_3", 4), "More arg variations."),
177 | modifiers=None,
178 | args=[
179 | Argument(loc, "self", Argument.Type.POSITIONAL, None, None, None),
180 | Argument(loc, "a", Argument.Type.POSITIONAL, None, "int", None),
181 | Argument(loc, "b", Argument.Type.POSITIONAL, None, None, None),
182 | Argument(loc, "args", Argument.Type.POSITIONAL_REMAINDER, None, None, None),
183 | Argument(loc, "opt", Argument.Type.KEYWORD_ONLY, None, "str", None),
184 | ],
185 | return_type="Optional[int]",
186 | decorations=[
187 | Decoration(Location("test_funcdef_3", 2), "classmethod", None),
188 | Decoration(Location("test_funcdef_3", 3), "db_session", "(sql_debug=True)"),
189 | ],
190 | )
191 | ]
192 |
193 |
194 | @docspec_test()
195 | def test_funcdef_4() -> List[_ModuleMemberType]:
196 | """
197 | def fun(project_name, project_type, port=8001):
198 | pass
199 | """
200 |
201 | return [
202 | Function(
203 | name="fun",
204 | location=loc,
205 | docstring=None,
206 | modifiers=None,
207 | args=[
208 | Argument(loc, "project_name", Argument.Type.POSITIONAL, None, None, None),
209 | Argument(loc, "project_type", Argument.Type.POSITIONAL, None, None, None),
210 | Argument(loc, "port", Argument.Type.POSITIONAL, None, None, "8001"),
211 | ],
212 | return_type=None,
213 | decorations=[],
214 | )
215 | ]
216 |
217 |
218 | @docspec_test(parser_options=ParserOptions(treat_singleline_comment_blocks_as_docstrings=True))
219 | def test_funcdef_5_single_stmt() -> List[_ModuleMemberType]:
220 | """
221 | def func1(self): return self.foo
222 |
223 | def func2(self):
224 | # ABC
225 | # DEF
226 | return self.foo
227 |
228 | def func3(self):
229 | ''' ABC
230 | DEF '''
231 | return self.foo
232 |
233 | def func4(self):
234 | '''
235 | ABC
236 | DEF
237 | '''
238 | return self.foo
239 | """
240 |
241 | args = [Argument(loc, "self", Argument.Type.POSITIONAL, None, None, None)]
242 | return [
243 | mkfunc("func1", None, 1, args),
244 | mkfunc("func2", "ABC\n DEF", 4, args),
245 | mkfunc("func3", "ABC\nDEF", 9, args),
246 | mkfunc("func4", "ABC\n DEF", 14, args),
247 | ]
248 |
249 |
250 | @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")
251 | @docspec_test()
252 | def test_funcdef_6_starred_args() -> List[_ModuleMemberType]:
253 | """
254 | def func1(a, *, b, **c): pass
255 |
256 | def func2(*args, **kwargs):
257 | ''' Docstring goes here. '''
258 |
259 | def func3(*, **kwargs):
260 | ''' Docstring goes here. '''
261 |
262 | def func4(abc, *,):
263 | '''Docstring goes here'''
264 |
265 | def func5(abc, *, kwonly):
266 | '''Docstring goes here'''
267 |
268 | async def func6(cls, *fs, loop=None, timeout=None, total=None, **tqdm_kwargs):
269 | ''' Docstring goes here. '''
270 | """
271 |
272 | return [
273 | mkfunc(
274 | "func1",
275 | None,
276 | 0,
277 | [
278 | Argument(loc, "a", Argument.Type.POSITIONAL, None, None, None),
279 | Argument(loc, "b", Argument.Type.KEYWORD_ONLY, None, None, None),
280 | Argument(loc, "c", Argument.Type.KEYWORD_REMAINDER, None, None, None),
281 | ],
282 | ),
283 | mkfunc(
284 | "func2",
285 | "Docstring goes here.",
286 | 4,
287 | [
288 | Argument(loc, "args", Argument.Type.POSITIONAL_REMAINDER, None, None, None),
289 | Argument(loc, "kwargs", Argument.Type.KEYWORD_REMAINDER, None, None, None),
290 | ],
291 | ),
292 | mkfunc(
293 | "func3",
294 | "Docstring goes here.",
295 | 7,
296 | [
297 | Argument(loc, "kwargs", Argument.Type.KEYWORD_REMAINDER, None, None, None),
298 | ],
299 | ),
300 | mkfunc(
301 | "func4",
302 | "Docstring goes here",
303 | 10,
304 | [
305 | Argument(loc, "abc", Argument.Type.POSITIONAL, None, None, None),
306 | ],
307 | ),
308 | mkfunc(
309 | "func5",
310 | "Docstring goes here",
311 | 13,
312 | [
313 | Argument(loc, "abc", Argument.Type.POSITIONAL, None, None, None),
314 | Argument(loc, "kwonly", Argument.Type.KEYWORD_ONLY, None, None, None),
315 | ],
316 | ),
317 | mkfunc(
318 | "func6",
319 | "Docstring goes here.",
320 | 16,
321 | [
322 | Argument(loc, "cls", Argument.Type.POSITIONAL, None, None, None),
323 | Argument(loc, "fs", Argument.Type.POSITIONAL_REMAINDER, None, None, None),
324 | Argument(loc, "loop", Argument.Type.KEYWORD_ONLY, None, None, "None"),
325 | Argument(loc, "timeout", Argument.Type.KEYWORD_ONLY, None, None, "None"),
326 | Argument(loc, "total", Argument.Type.KEYWORD_ONLY, None, None, "None"),
327 | Argument(loc, "tqdm_kwargs", Argument.Type.KEYWORD_REMAINDER, None, None, None),
328 | ],
329 | ["async"],
330 | ),
331 | ]
332 |
333 |
334 | @pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher")
335 | @docspec_test()
336 | def test_funcdef_7_posonly_args() -> List[_ModuleMemberType]:
337 | """
338 | def func1(x, y=3, /, z=5, w=7): pass
339 | def func2(x, /, *v, a=1, b=2): pass
340 | def func3(x, /, *, a=1, b=2, **kwargs): pass
341 | def func4(x, y, /): pass
342 | """
343 |
344 | return [
345 | mkfunc(
346 | "func1",
347 | None,
348 | 1,
349 | [
350 | Argument(loc, "x", Argument.Type.POSITIONAL_ONLY),
351 | Argument(loc, "y", Argument.Type.POSITIONAL_ONLY, default_value="3"),
352 | Argument(loc, "z", Argument.Type.POSITIONAL, default_value="5"),
353 | Argument(loc, "w", Argument.Type.POSITIONAL, default_value="7"),
354 | ],
355 | ),
356 | mkfunc(
357 | "func2",
358 | None,
359 | 2,
360 | [
361 | Argument(loc, "x", Argument.Type.POSITIONAL_ONLY),
362 | Argument(loc, "v", Argument.Type.POSITIONAL_REMAINDER),
363 | Argument(loc, "a", Argument.Type.KEYWORD_ONLY, default_value="1"),
364 | Argument(loc, "b", Argument.Type.KEYWORD_ONLY, default_value="2"),
365 | ],
366 | ),
367 | mkfunc(
368 | "func3",
369 | None,
370 | 3,
371 | [
372 | Argument(loc, "x", Argument.Type.POSITIONAL_ONLY),
373 | Argument(loc, "a", Argument.Type.KEYWORD_ONLY, default_value="1"),
374 | Argument(loc, "b", Argument.Type.KEYWORD_ONLY, default_value="2"),
375 | Argument(loc, "kwargs", Argument.Type.KEYWORD_REMAINDER),
376 | ],
377 | ),
378 | mkfunc(
379 | "func4",
380 | None,
381 | 3,
382 | [
383 | Argument(loc, "x", Argument.Type.POSITIONAL_ONLY),
384 | Argument(loc, "y", Argument.Type.POSITIONAL_ONLY),
385 | ],
386 | ),
387 | ]
388 |
389 |
390 | @docspec_test()
391 | def test_classdef_1_exceptions() -> List[_ModuleMemberType]:
392 | """
393 | class MyError1:
394 | pass
395 |
396 | class MyError2():
397 | pass
398 |
399 | class MyError3(RuntimeError):
400 | pass
401 |
402 | class MyError4(RuntimeError, object, metaclass=ABCMeta):
403 | pass
404 |
405 | class MyError5(metaclass=ABCMeta):
406 | pass
407 |
408 | class MyError6(RuntimeError):
409 | __metaclass__ = ABCMeta
410 | """
411 |
412 | return [
413 | Class(name="MyError1", location=loc, docstring=None, metaclass=None, bases=[], decorations=None, members=[]),
414 | Class(name="MyError2", location=loc, docstring=None, metaclass=None, bases=[], decorations=None, members=[]),
415 | Class(
416 | name="MyError3",
417 | location=loc,
418 | docstring=None,
419 | metaclass=None,
420 | bases=["RuntimeError"],
421 | decorations=None,
422 | members=[],
423 | ),
424 | Class(
425 | name="MyError4",
426 | location=loc,
427 | docstring=None,
428 | metaclass="ABCMeta",
429 | bases=["RuntimeError", "object"],
430 | decorations=None,
431 | members=[],
432 | ),
433 | Class(
434 | name="MyError5", location=loc, docstring=None, metaclass="ABCMeta", bases=[], decorations=None, members=[]
435 | ),
436 | Class(
437 | name="MyError6",
438 | location=loc,
439 | docstring=None,
440 | metaclass="ABCMeta",
441 | bases=["RuntimeError"],
442 | decorations=None,
443 | members=[],
444 | ),
445 | ]
446 |
447 |
448 | @docspec_test(strip_locations=False)
449 | def test_indirections() -> List[_ModuleMemberType]:
450 | """
451 | import os
452 | import urllib.request as r
453 | import os.path, \
454 | sys, pathlib as P
455 | from sys import platform, executable as EXE
456 | from os.path import *
457 | from pathlib import (
458 | PurePath as PP,
459 | PosixPath
460 | )
461 | from .. import core
462 | from ..core import Widget, View
463 | from .vendor import pkg_resources, six
464 | from ...api import *
465 | def foo():
466 | from sys import platform
467 | class bar:
468 | import os
469 | from os.path import dirname
470 | """
471 |
472 | return [
473 | Indirection(Location("test_indirections", 2), "os", None, "os"),
474 | Indirection(Location("test_indirections", 3), "r", None, "urllib.request"),
475 | Indirection(Location("test_indirections", 4), "path", None, "os.path"),
476 | Indirection(Location("test_indirections", 4), "sys", None, "sys"),
477 | Indirection(Location("test_indirections", 4), "P", None, "pathlib"),
478 | Indirection(Location("test_indirections", 5), "platform", None, "sys.platform"),
479 | Indirection(Location("test_indirections", 5), "EXE", None, "sys.executable"),
480 | Indirection(Location("test_indirections", 6), "*", None, "os.path.*"),
481 | Indirection(Location("test_indirections", 8), "PP", None, "pathlib.PurePath"),
482 | Indirection(Location("test_indirections", 9), "PosixPath", None, "pathlib.PosixPath"),
483 | Indirection(Location("test_indirections", 11), "core", None, "..core"),
484 | Indirection(Location("test_indirections", 12), "Widget", None, "..core.Widget"),
485 | Indirection(Location("test_indirections", 12), "View", None, "..core.View"),
486 | Indirection(Location("test_indirections", 13), "pkg_resources", None, ".vendor.pkg_resources"),
487 | Indirection(Location("test_indirections", 13), "six", None, ".vendor.six"),
488 | Indirection(Location("test_indirections", 14), "*", None, "...api.*"),
489 | Function(Location("test_indirections", 15), "foo", None, None, [], None, []),
490 | Class(
491 | Location("test_indirections", 17),
492 | "bar",
493 | None,
494 | [
495 | Indirection(Location("test_indirections", 18), "os", None, "os"),
496 | Indirection(Location("test_indirections", 19), "dirname", None, "os.path.dirname"),
497 | ],
498 | None,
499 | [],
500 | None,
501 | ),
502 | ]
503 |
504 |
505 | def test_format_arglist() -> None:
506 | func = mkfunc(
507 | "func6",
508 | "Docstring goes here.",
509 | 16,
510 | [
511 | Argument(loc, "cls", Argument.Type.POSITIONAL, None, None, None),
512 | Argument(loc, "fs", Argument.Type.POSITIONAL_REMAINDER, None, None, None),
513 | Argument(loc, "loop", Argument.Type.KEYWORD_ONLY, None, None, "None"),
514 | Argument(loc, "timeout", Argument.Type.KEYWORD_ONLY, None, None, "None"),
515 | Argument(loc, "total", Argument.Type.KEYWORD_ONLY, None, None, "None"),
516 | Argument(loc, "tqdm_kwargs", Argument.Type.KEYWORD_REMAINDER, None, None, None),
517 | ],
518 | ["async"],
519 | )
520 | assert format_arglist(func.args, True) == "cls, *fs, loop=None, timeout=None, total=None, **tqdm_kwargs"
521 |
522 |
523 | @docspec_test()
524 | def test_funcdef_with_trailing_comma() -> List[_ModuleMemberType]:
525 | """
526 | def build_docker_image(
527 | name: str = "buildDocker",
528 | default: bool = False,
529 | dockerfile: str = "docker/release.Dockerfile",
530 | project: Project | None = None,
531 | auth: dict[str, tuple[str, str]] | None = None,
532 | secrets: dict[str, str] | None = None,
533 | image_qualifier: str | None = None,
534 | platforms: list[str] | None = None,
535 | **kwargs: Any,
536 | ) -> Task:
537 | pass
538 | """
539 |
540 | return [
541 | mkfunc(
542 | "build_docker_image",
543 | None,
544 | 0,
545 | [
546 | Argument(loc, "name", Argument.Type.POSITIONAL, None, "str", '"buildDocker"'),
547 | Argument(loc, "default", Argument.Type.POSITIONAL, None, "bool", "False"),
548 | Argument(loc, "dockerfile", Argument.Type.POSITIONAL, None, "str", '"docker/release.Dockerfile"'),
549 | Argument(loc, "project", Argument.Type.POSITIONAL, None, "Project | None", "None"),
550 | Argument(loc, "auth", Argument.Type.POSITIONAL, None, "dict[str, tuple[str, str]] | None", "None"),
551 | Argument(loc, "secrets", Argument.Type.POSITIONAL, None, "dict[str, str] | None", "None"),
552 | Argument(loc, "image_qualifier", Argument.Type.POSITIONAL, None, "str | None", "None"),
553 | Argument(loc, "platforms", Argument.Type.POSITIONAL, None, "list[str] | None", "None"),
554 | Argument(loc, "kwargs", Argument.Type.KEYWORD_REMAINDER, None, "Any", None),
555 | ],
556 | return_type="Task",
557 | ),
558 | ]
559 |
560 |
561 | @docspec_test()
562 | def test_funcdef_with_match_statement() -> List[_ModuleMemberType]:
563 | """
564 | def f(x):
565 | match x:
566 | case str(s):
567 | return "string"
568 | case Path() as p:
569 | return "path"
570 | case int(n) | float(n):
571 | return "number"
572 | case _:
573 | return "idk"
574 | """
575 |
576 | return [
577 | mkfunc(
578 | "f",
579 | None,
580 | 0,
581 | [
582 | Argument(loc, "x", Argument.Type.POSITIONAL, None),
583 | ],
584 | ),
585 | ]
586 |
587 |
588 | @docspec_test(strip_locations=True)
589 | def test_can_parse_raw_docstring() -> List[_ModuleMemberType]:
590 | r"""
591 | def normal():
592 | ' Normal d\\cstring. '
593 |
594 | def single():
595 | r' S\\ngle raw docstring. '
596 |
597 | def multi():
598 | r''' M\\lti raw docstring. '''
599 |
600 | def special_characters():
601 | ''' ff '''
602 | """
603 |
604 | return [
605 | mkfunc("normal", "Normal d\\cstring.", 0, []),
606 | mkfunc("single", "S\\\\ngle raw docstring.", 0, []),
607 | mkfunc("multi", "M\\\\lti raw docstring.", 0, []),
608 | mkfunc("special_characters", "ff", 0, []),
609 | ]
610 |
611 |
612 | @docspec_test(strip_locations=True)
613 | def test_can_detect_docstrings_after_declarations() -> List[_ModuleMemberType]:
614 | """
615 | class Test:
616 | a: int
617 | ''' This attribute is important. '''
618 |
619 | #: And so is this.
620 | b: str
621 | """
622 |
623 | return [
624 | Class(
625 | loc,
626 | "Test",
627 | None,
628 | [
629 | Variable(
630 | loc,
631 | "a",
632 | Docstring(loc, "This attribute is important."),
633 | "int",
634 | ),
635 | Variable(
636 | loc,
637 | "b",
638 | Docstring(loc, "And so is this."),
639 | "str",
640 | ),
641 | ],
642 | None,
643 | [],
644 | None,
645 | )
646 | ]
647 |
648 |
649 | @docspec_test(strip_locations=True)
650 | def test_can_parse_tuple_unpacking() -> List[_ModuleMemberType]:
651 | """
652 | v = 42
653 |
654 | a, b = 0, 1 #: This documents a and b.
655 |
656 | #: This documents c and d.
657 | c, *d = it
658 |
659 | e, (f, *g) = value
660 | """
661 |
662 | # NOTE(NiklasRosenstein): We don't explicitly support yielding information about variables
663 | # resulting from tuple-unpacking as we cannot tell which of the variables the docstring is
664 | # for, and how to assign the right side to the variables on the left.
665 |
666 | return [Variable(loc, "v", None, None, "42")]
667 |
668 |
669 | @docspec_test(strip_locations=True)
670 | def test_can_parse_docstrings_on_same_line() -> List[_ModuleMemberType]:
671 | """
672 | a: int = 42 #: This is a variable.
673 |
674 | class Test:
675 | b: str #: This is b variable.
676 |
677 | #: This takes precedence!
678 | c: float #: This is c variable.
679 |
680 | d: None #: This is also ignored.
681 | ''' Because I exist! '''
682 | """
683 |
684 | return [
685 | Variable(loc, "a", Docstring(loc, "This is a variable."), "int", "42"),
686 | Class(
687 | loc,
688 | "Test",
689 | None,
690 | [
691 | Variable(loc, "b", Docstring(loc, "This is b variable."), "str"),
692 | ],
693 | None,
694 | [],
695 | None,
696 | ),
697 | Variable(loc, "c", Docstring(loc, "This takes precedence!"), "float"),
698 | Variable(loc, "d", Docstring(loc, "Because I exist!"), "None"),
699 | ]
700 |
701 |
702 | @docspec_test(strip_locations=True)
703 | def test_hash_docstring_does_not_loose_indentation() -> List[_ModuleMemberType]:
704 | """
705 | #: Represents this command:
706 | #:
707 | #: $ bash ./install.sh
708 | #:
709 | #:Ok?
710 | command = ["bash", "./install.sh"]
711 | """
712 |
713 | return [
714 | Variable(
715 | loc,
716 | "command",
717 | Docstring(
718 | loc,
719 | dedent(
720 | """
721 | Represents this command:
722 |
723 | $ bash ./install.sh
724 |
725 | Ok?
726 | """
727 | ).strip(),
728 | ),
729 | None,
730 | '["bash", "./install.sh"]',
731 | )
732 | ]
733 |
734 |
735 | @docspec_test(strip_locations=True)
736 | def test_docstring_does_not_loose_indentation() -> List[_ModuleMemberType]:
737 | """
738 | def foo():
739 | '''
740 | Example:
741 |
742 | assert 42 == "Answer to the universe"
743 | '''
744 | """
745 |
746 | return [
747 | mkfunc(
748 | "foo",
749 | dedent(
750 | """
751 | Example:
752 |
753 | assert 42 == "Answer to the universe"
754 | """
755 | ).strip(),
756 | 0,
757 | [],
758 | )
759 | ]
760 |
761 |
762 | @docspec_test(strip_locations=True)
763 | def test_octal_escape_sequence() -> List[_ModuleMemberType]:
764 | r"""
765 | def my_example():
766 | 'This is supposed to be pound: \043'
767 | pass
768 | """
769 |
770 | return [mkfunc("my_example", "This is supposed to be pound: #", 0, [])]
771 |
--------------------------------------------------------------------------------
/docspec/.changelog/0.2.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2020-07-06"
2 |
3 | [[entries]]
4 | id = "85dee980-a399-4566-9fc9-f31812cc77e3"
5 | type = "improvement"
6 | description = "`ApiObject` constructors now accept keyword arguments only"
7 | author = "@NiklasRosenstein"
8 |
9 | [[entries]]
10 | id = "8ba1b730-c869-44fb-971e-fe196a495fb6"
11 | type = "improvement"
12 | description = "All `nullable` fields now have `default=None`"
13 | author = "@NiklasRosenstein"
14 |
15 | [[entries]]
16 | id = "6d53f317-a8e9-4e6d-80dc-c714f24af96e"
17 | type = "improvement"
18 | description = "Many serializable classes are now decorated with `SkipDefaults()`"
19 | author = "@NiklasRosenstein"
20 |
--------------------------------------------------------------------------------
/docspec/.changelog/0.2.1.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-02-21"
2 |
3 | [[entries]]
4 | id = "0e28a249-3e4f-4a27-a71e-566686316a3c"
5 | type = "fix"
6 | description = "fix dependencies"
7 | author = "@NiklasRosenstein"
8 |
--------------------------------------------------------------------------------
/docspec/.changelog/1.0.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-07-21"
2 |
3 | [[entries]]
4 | id = "56f48acf-c782-4217-b4de-ba106d2819c3"
5 | type = "breaking change"
6 | description = "Migrate to using `databind.core 1.x` and `databind.json 1.x` from `nr.databind.core` and `nr.databind.json`."
7 | author = "@NiklasRosenstein"
8 |
--------------------------------------------------------------------------------
/docspec/.changelog/1.0.1.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-07-22"
2 |
3 | [[entries]]
4 | id = "2af08e6b-0d74-4a0c-aa39-f3639b48c311"
5 | type = "fix"
6 | description = "`docspec.get_members()` access object members wrong"
7 | author = "@NiklasRosenstein"
8 |
--------------------------------------------------------------------------------
/docspec/.changelog/1.0.2.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-07-29"
2 |
3 | [[entries]]
4 | id = "972ae012-8d3a-46e2-be70-5a34503e1738"
5 | type = "fix"
6 | description = "Add some type annotations"
7 | author = "@tristanlatr"
8 | issues = [
9 | "https://github.com/NiklasRosenstein/docspec/issues/14",
10 | ]
11 |
--------------------------------------------------------------------------------
/docspec/.changelog/1.1.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-08-27"
2 |
3 | [[entries]]
4 | id = "30498e89-71b7-4ae4-9099-01a67275dda8"
5 | type = "feature"
6 | description = "add `ApiObject.parent` and `ApiObject.path` to deprecate `ReverseMap` class"
7 | author = "@NiklasRosenstein"
8 |
9 | [[entries]]
10 | id = "787d377f-b2b9-4797-8261-fb213107ba1d"
11 | type = "feature"
12 | description = "add `ApiObject.sync_hierarchy()` method"
13 | author = "@NiklasRosenstein"
14 |
15 | [[entries]]
16 | id = "0c527d2f-97b6-4171-97e4-70dbe1638693"
17 | type = "feature"
18 | description = "introduce `HasMembers` base class for `Class` and `Module`, `Module.members` can now container other modules"
19 | author = "@NiklasRosenstein"
20 | issues = [
21 | "https://github.com/NiklasRosenstein/docspec/issues/15",
22 | ]
23 |
24 | [[entries]]
25 | id = "e53522b9-725f-4307-86f7-a503ba7d678b"
26 | type = "improvement"
27 | description = "`get_member()` and `filter_visit()` have been updated to make use of the `HasMembers` base class instead of relying on `hasattr()`/`getattr()`"
28 | author = "@NiklasRosenstein"
29 |
30 | [[entries]]
31 | id = "d0e2adff-4327-4956-b255-233a34e05f35"
32 | type = "feature"
33 | description = "add `Indirection` class to keep track of imports and resolve names correctly."
34 | author = "@tristanlatr"
35 |
36 | [[entries]]
37 | id = "c1951174-9a5a-4e90-b03f-3c10219559c9"
38 | type = "feature"
39 | description = "add `Docstring` class which represents the docstring, plus the location of the docstring"
40 | author = "@NiklasRosenstein"
41 |
--------------------------------------------------------------------------------
/docspec/.changelog/1.2.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2021-09-24"
2 |
3 | [[entries]]
4 | id = "5ea419a6-7812-472d-a9ff-1c3712e03052"
5 | type = "feature"
6 | description = "add `Data.modifiers`, `Data.semantic_hints`, `Class.modifiers`, `Class.semantic_hints` and `Function.semantic_hints`"
7 | author = "@NiklasRosenstein"
8 | issues = [
9 | "https://github.com/NiklasRosenstein/docspec/issues/30",
10 | ]
11 |
12 | [[entries]]
13 | id = "bd703a65-ac7b-40ca-82ee-05389bc83e65"
14 | type = "improvement"
15 | description = "rename `Argument.Type` enumeration values to `UPPER_CASE` format according to latest specification (full backwards compatibility, including deserializing JSON payloads with the old argument type names)"
16 | author = "@NiklasRosenstein"
17 |
18 | [[entries]]
19 | id = "d154ccd2-4bf5-4179-856f-ec76ab424b7b"
20 | type = "feature"
21 | description = "add `Argument.location` and `Decoration.location` properties"
22 | author = "@NiklasRosenstein"
23 | issues = [
24 | "https://github.com/NiklasRosenstein/docspec/issues/28",
25 | ]
26 |
27 | [[entries]]
28 | id = "5b04794e-c685-4471-b2a8-d043fecf30fd"
29 | type = "feature"
30 | description = "add `Location.endlineno` property to spec"
31 | author = "@NiklasRosenstein"
32 | issues = [
33 | "https://github.com/NiklasRosenstein/docspec/issues/32",
34 | ]
35 |
36 | [[entries]]
37 | id = "e8ab1135-3cc6-4312-b2c8-982526755c51"
38 | type = "improvement"
39 | description = "add `Decoration.arglist`, document that `Decoration.args` is deprecated"
40 | author = "@NiklasRosenstein"
41 |
--------------------------------------------------------------------------------
/docspec/.changelog/2.0.0a1.toml:
--------------------------------------------------------------------------------
1 | release-date = "2022-02-24"
2 |
3 | [[entries]]
4 | id = "0fb0c132-6952-4f06-93a6-c285321e812d"
5 | type = "fix"
6 | description = "update `specification.yml` to show correct types of `Argument.location`, `Decoration.location` and `Module.location`"
7 | author = "@NiklasRosenstein"
8 | pr = "https://github.com/NiklasRosenstein/docspec/pull/56"
9 | issues = [
10 | "https://github.com/NiklasRosenstein/docspec/issues/52",
11 | ]
12 |
13 | [[entries]]
14 | id = "283442c8-8f22-4236-bc20-fc38de7cd587"
15 | type = "breaking change"
16 | description = "remove deprecated class `ReverseMap`"
17 | author = "@NiklasRosenstein"
18 | pr = "https://github.com/NiklasRosenstein/docspec/pull/63"
19 |
20 | [[entries]]
21 | id = "e3629fc5-e3e3-4c46-a6ab-d5100aa7f72a"
22 | type = "breaking change"
23 | description = "`Docstring` class no longer inherits from `str` and is no longer frozen"
24 | author = "@NiklasRosenstein"
25 | pr = "https://github.com/NiklasRosenstein/docspec/pull/64"
26 | issues = [
27 | "https://github.com/NiklasRosenstein/docspec/issues/49",
28 | ]
29 |
30 | [[entries]]
31 | id = "c97dec50-a34b-49ad-9dc5-60b693524c03"
32 | type = "breaking change"
33 | description = "rename `Data` to `Variable`"
34 | author = "@NiklasRosenstein"
35 | pr = "https://github.com/NiklasRosenstein/docspec/pull/68"
36 | issues = [
37 | "https://github.com/NiklasRosenstein/docspec/issues/67",
38 | ]
39 |
40 | [[entries]]
41 | id = "300029d9-3c6f-43b6-989b-44603803c623"
42 | type = "hygiene"
43 | description = "move `VariableSemantic`, `FunctionSemantic` and `ClassSemantic` into global scope"
44 | author = "@NiklasRosenstein"
45 |
46 | [[entries]]
47 | id = "e76db342-38d4-4a06-a664-653f4a73613f"
48 | type = "improvement"
49 | description = "`filter_visit()` and `visit()` now expect a `MutableSequence` instead of `List`"
50 | author = "@NiklasRosenstein"
51 |
52 | [[entries]]
53 | id = "d11aef53-f885-469c-b73e-2a056895fb6f"
54 | type = "hygiene"
55 | description = "remove `files` argument from `load_python_modules()`"
56 | author = "@NiklasRosenstein"
57 |
58 | [[entries]]
59 | id = "6b647706-2391-46ee-99c7-a0af9f77bb34"
60 | type = "improvement"
61 | description = "declare two overloads for `parse_python_module()` for reading from a filename or a file-like object"
62 | author = "@NiklasRosenstein"
63 |
64 | [[entries]]
65 | id = "a313a23b-5ddb-4057-a251-bc4ccb6d32c4"
66 | type = "breaking change"
67 | description = "`Location.filename` is no longer optional as per the specification"
68 | author = "@NiklasRosenstein"
69 |
70 | [[entries]]
71 | id = "db9fab76-4f70-40f0-ad53-afe3a0ddcedb"
72 | type = "breaking change"
73 | description = "harden requirements of spec by requiring a `location` even on `Decoration` and `Argument` objects, and the location is no longer optional. Rearrange the argument the Python classes such that the `location` argument comes first, always"
74 | author = "@NiklasRosenstein"
75 | pr = "https://github.com/NiklasRosenstein/docspec/pull/70"
76 |
77 | [[entries]]
78 | id = "eebe19e0-6339-4d9f-acdc-838c0b29ffe2"
79 | type = "breaking change"
80 | description = "remove `file` argument from `docspec-python` CLI"
81 | author = "@NiklasRosenstein"
82 | pr = "https://github.com/NiklasRosenstein/docspec/pull/70"
83 |
--------------------------------------------------------------------------------
/docspec/.changelog/2.1.2.toml:
--------------------------------------------------------------------------------
1 | release-date = "2023-03-15"
2 |
3 | [[entries]]
4 | id = "9079a43e-623e-4c34-86b2-075da533e819"
5 | type = "improvement"
6 | description = "Introduce `HasLocation` base class which now serves as the base for `Docstring`, `Decoration`, `Argument` and `ApiObject`."
7 | author = "@NiklasRosenstein"
8 |
--------------------------------------------------------------------------------
/docspec/.changelog/2.2.0.toml:
--------------------------------------------------------------------------------
1 | release-date = "2023-05-28"
2 |
3 | [[entries]]
4 | id = "b1de9a35-e65a-4b0e-8739-efc098aad35c"
5 | type = "improvement"
6 | description = "Upgrade to databind `4.2.6`"
7 | author = "@NiklasRosenstein"
8 |
9 | [[entries]]
10 | id = "c7062ee2-6aa1-4e61-be7e-13bd130035b2"
11 | type = "feature"
12 | description = "Add `dump_module(serialize_defaults)` parameter"
13 | author = "@NiklasRosenstein"
14 |
--------------------------------------------------------------------------------
/docspec/.changelog/2.2.1.toml:
--------------------------------------------------------------------------------
1 | release-date = "2023-05-28"
2 |
3 | [[entries]]
4 | id = "4be7552a-bb3c-4f80-babf-81179350bc64"
5 | type = "fix"
6 | description = "Fix databind dependency names. The packages are named `databind.core` and `databind.json`, respectively, and referencing with a `-` instead (the normalized form) can fail in older Python versions (3.7 - 3.9, it seems) when using the `pkg_resources` module (example error: `pkg_resources.DistributionNotFound: The 'databind-json<5.0.0,>=4.2.6' distribution was not found and is required by docspec`)."
7 | author = "@NiklasRosenstein"
8 |
--------------------------------------------------------------------------------
/docspec/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 120
3 | # Black can yield formatted code that triggers these Flake8 warnings.
4 | ignore=
5 | # line break before binary operator
6 | W503,
7 | # line break after binary operator
8 | W504,
9 |
--------------------------------------------------------------------------------
/docspec/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "docspec"
7 | version = "2.2.2"
8 | description = "Docspec is a JSON object specification for representing API documentation of programming languages."
9 | readme = "readme.md"
10 | requires-python = ">=3.8"
11 | dependencies = [
12 | "databind-core>=4.2.6",
13 | "databind-json>=4.2.6",
14 | "deprecated>=1.2.12",
15 | ]
16 | authors = [{ name = "Niklas Rosenstein", email = "rosensteinniklas@gmail.com" }]
17 |
18 | [project.scripts]
19 | docspec = "docspec.__main__:main"
20 |
21 | [tool.uv]
22 | dev-dependencies = [
23 | "mypy>=1.13.0",
24 | "pytest>=8.3.4",
25 | "types-deprecated>=1.2.15.20241117",
26 | "types-termcolor>=1.1.6.2",
27 | ]
28 |
29 | [tool.mypy]
30 | python_version = "3.8"
31 | explicit_package_bases = true
32 | mypy_path = ["src"]
33 | namespace_packages = true
34 | pretty = true
35 | show_error_codes = true
36 | show_error_context = true
37 | strict = true
38 | warn_no_return = true
39 | warn_redundant_casts = true
40 | warn_unreachable = true
41 | warn_unused_ignores = true
42 | check_untyped_defs = true
43 |
44 | [tool.ruff]
45 | line-length = 120
46 |
--------------------------------------------------------------------------------
/docspec/readme.md:
--------------------------------------------------------------------------------
1 | # docspec
2 |
3 | This Python packages provides
4 |
5 | * A library to (de-) serialize Docspec conformat JSON payloads
6 | * A CLI to validate and introspect such payloads
7 |
8 | Example:
9 |
10 | ```py
11 | import docspec, sys
12 | for module in docspec.load_modules(sys.stdin):
13 | module.members = [member for member in module.members if member.docstring]
14 | docspec.dump_module(sys.stdout)
15 | ```
16 |
17 | ```
18 | $ docspec module.json --dump-tree
19 | module docspec
20 | | class Location
21 | | | data filename
22 | | | data lineno
23 | | class Decoration
24 | | | data name
25 | # ...
26 | ```
27 |
28 | The `docspec` Python module requires Python 3.5 or newer.
29 |
30 | ---
31 |
32 | Copyright © 2020, Niklas Rosenstein
33 |
--------------------------------------------------------------------------------
/docspec/specification.yml:
--------------------------------------------------------------------------------
1 |
2 | Location:
3 | type: struct
4 | docs: The location object describes where the an API object was extracted from a
5 | file. Uusally this points to the source file and a line number. The filename
6 | should always be relative to the root of a project or source control repository.
7 | fields:
8 | filename:
9 | type: str
10 | docs: A relative filename (e.g. relative to the project root).
11 | lineno:
12 | type: int
13 | docs: The line number in the *filename* from which the API object was parsed.
14 | endlineno:
15 | type: Optional[int]
16 | required: false
17 | docs: If the location of an entity spans over multiple lines, it can be indicated by specifying at
18 | which line it ends with this property.
19 |
20 | Docstring:
21 | type: struct
22 | docs: Represents the documentation string of an API object.
23 | fields:
24 | location:
25 | type: Location
26 | docs: The location where the docstring is defined. This points at the position of
27 | the first character in the *content* field.
28 | content:
29 | type: str
30 | docs: The content of the docstring.
31 |
32 | Indirection:
33 | type: struct
34 | docs: Represents an imported name. It can be used to resolve references to names
35 | in the API tree to fully qualified names.
36 | fields:
37 | type:
38 | type: str
39 | docs: The value is `"indirection"`.
40 | location:
41 | type: Location
42 | docs: The location where the indirection is defined.
43 | name:
44 | type: str
45 | docs: The name that is made available in the scope of the parent object.
46 | target:
47 | type: str
48 | docs: The target to which the name points. In the case of Python for example this
49 | can be a fully qualified name pointing to a member or a member of a module. In
50 | the case of starred imports, the last part is a star (as in `os.path.*`).
51 |
52 | Variable:
53 | type: struct
54 | docs: A `Variable` object represents a variable or constant.
55 | fields:
56 | type:
57 | type: str
58 | docs: The value is `"data"`.
59 | location:
60 | type: Location
61 | docs: The location where the variable or constant is defined.
62 | name:
63 | type: str
64 | docs: The name of the variable or constant.
65 | docstring:
66 | type: Optional[Docstring]
67 | required: false
68 | docs: The docstring of the variable or constant.
69 | datatype:
70 | type: Optional[str]
71 | required: false
72 | docs: The name of the type of the variable or constant.
73 | value:
74 | type: Optional[str]
75 | required: false
76 | docs: The value that is assigned to this variable or constant as source code.
77 | modifiers:
78 | type: Optional[List[str]]
79 | required: false
80 | docs: A list of modifier keywords used in the source code to define this variable or
81 | constant, like `const`, `static`, `final`, `mut`, etc.
82 | semantic_hints:
83 | type: List[VariableSemantic]
84 | required: false
85 | docs: A list of behavioral properties for this variable or constant.
86 |
87 | VariableSemantic:
88 | type: enum
89 | docs: Describes possible behavioral properties of a variable or constant.
90 | values:
91 | - name: INSTANCE_VARIABLE
92 | - name: CLASS_VARIABLE
93 | - name: CONSTANT
94 |
95 | Argument:
96 | type: struct
97 | docs: Represents a function argument.
98 | fields:
99 | location:
100 | type: Location
101 | docs: The location of the decoration in the source code.
102 | name:
103 | type: str
104 | docs: The name of the argument.
105 | type:
106 | type: ArgumentType
107 | docs: The type of argument.
108 | datatype:
109 | type: Optional[str]
110 | required: false
111 | docs: The data type of the argument.
112 | default_value:
113 | type: Optional[str]
114 | required: false
115 | docs: The default value of the argument as a code string.
116 |
117 | ArgumentType:
118 | type: enum
119 | values:
120 | - name: POSITIONAL_ONLY
121 | docs: An argument that can only be given by its position in the argument list. In Python,
122 | these are arguments preceeding a `/` marker in the argument list. Many programming languages
123 | support only one type of positional arguments. Loaders for such languages should prefer the
124 | `POSITIONAL` argument type over `POSITIONAL_ONLY` to describe these type of arguments.
125 | - name: POSITIONAL
126 | - name: POSITIONAL_REMAINDER
127 | - name: KEYWORD_ONLY
128 | - name: KEYWORD_REMAINDER
129 |
130 | Decoration:
131 | type: struct
132 | docs: Represents a decoration that can be applied to a function or class.
133 | fields:
134 | location:
135 | type: Location
136 | docs: The location of the decoration in the source code.
137 | name:
138 | type: str
139 | docs: The name of the decorator used in this decoration. This may be a piece of code in languages
140 | that support complex decoration syntax. (e.g. in Python, `@(decorator_factory().dec)(a, b, c)` should
141 | be represented as `"(decorator_factory().dec)"` for the `name` and `["a", "b", "c"]` for the `args`).
142 | args:
143 | type: Optional[str]
144 | required: false
145 | docs: Deprecated in favor of `arglist`. A single string that represents the entirety of the argument list
146 | for the decorator, excluding the surroinding parentheses.
147 | arglist:
148 | type: Optional[List[str]]
149 | required: false
150 | docs: A list of the raw source code for each argument of the decorator. If this is not set,
151 | that means the decorator is not called. If the list is empty, the decorator is called without
152 | arguments. For example if the full decoration code is `@(decorator_factory().dec)(a, b, c)`, this
153 | field's value would be `["a", "b", "c"]`.
154 |
155 | Function:
156 | type: struct
157 | docs: Represents a function definition in a module or class.
158 | fields:
159 | type:
160 | type: str
161 | docs: Value is `"function"`
162 | location:
163 | type: Location
164 | name:
165 | type: str
166 | docs: The name of the function.
167 | docstring:
168 | type: Optional[Docstring]
169 | required: false
170 | modifiers:
171 | type: Optional[List[str]]
172 | required: false
173 | docs: An list of modifier keywords that the function was defined with.
174 | args:
175 | type: List[Argument]
176 | docs: The function arguments.
177 | return_type:
178 | type: Optional[str]
179 | required: false
180 | docs: The return type of the function.
181 | decorations:
182 | type: Optional[List[Decoration]]
183 | required: false
184 | docs: The list of decorations attached to the function.
185 | semantic_hints:
186 | type: List[FunctionSemantic]
187 | required: false
188 | docs: A list of behavioral properties for this function.
189 |
190 | FunctionSemantic:
191 | type: enum
192 | values:
193 | - name: ABSTRACT
194 | - name: FINAL
195 | - name: COROUTINE
196 | - name: NO_RETURN
197 | - name: INSTANCE_METHOD
198 | - name: CLASS_METHOD
199 | - name: STATIC_METHOD
200 | - name: PROPERTY_GETTER
201 | - name: PROPERTY_SETTER
202 | - name: PROPERTY_DELETER
203 |
204 | Class:
205 | type: struct
206 | docs: Represents a class definition.
207 | fields:
208 | type:
209 | type: str
210 | docs: The value is `"class"`.
211 | location:
212 | type: Location
213 | name:
214 | type: str
215 | docs: The name of the class.
216 | docstring:
217 | type: Optional[Docstring]
218 | required: false
219 | metaclass:
220 | type: Optional[str]
221 | required: false
222 | docs: The name of the metaclass used in this class definition.
223 | bases:
224 | type: Optional[List[str]]
225 | required: false
226 | docs: A list of the base classes that the class inherits from.
227 | members:
228 | type: List[Variable | Function | Class]
229 | docs: A list of the members of the class.
230 | decorations:
231 | type: Optional[List[Decoration]]
232 | required: false
233 | docs: A list of the decorations applied to the class definition.
234 | modifiers:
235 | type: Optional[List[str]]
236 | required: false
237 | docs: A list of the modifier keywords used to declare this class.
238 | semantic_hints:
239 | type: List[ClassSemantic]
240 | required: false
241 | docs: A list of the semantic hints for this class.
242 |
243 | ClassSemantic:
244 | type: enum
245 | values:
246 | - name: INTERFACE
247 | - name: ABSTRACT
248 | - name: FINAL
249 | - name: ENUM
250 |
251 | Module:
252 | type: struct
253 | docs: A module represents a collection of data, function and classes. In the Python
254 | language, it represents an actual Python module. In other languages it may
255 | refer to another file type or a namespace.
256 | fields:
257 | type:
258 | type: str
259 | docs: The value is `"module"`.
260 | location:
261 | type: Location
262 | docs: The location of the module. Usually the line number will be `0`.
263 | name:
264 | type: str
265 | docs: The name of the module. The name is supposed to be relative to the parent.
266 | docstring:
267 | type: Optional[Docstring]
268 | required: false
269 | docs: The docstring for the module as parsed from the source.
270 | members:
271 | type: List[Class | Variable | Function | Module]
272 | docs: A list of the module members.
273 |
--------------------------------------------------------------------------------
/docspec/src/docspec/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf8 -*-
2 | # Copyright (c) 2021 Niklas Rosenstein
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to
6 | # deal in the Software without restriction, including without limitation the
7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8 | # sell copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in
12 | # all copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20 | # IN THE SOFTWARE.
21 |
22 | __author__ = "Niklas Rosenstein "
23 | __version__ = "2.2.1"
24 | __all__ = [
25 | "Location",
26 | "Decoration",
27 | "Docstring",
28 | "Argument",
29 | "ApiObject",
30 | "Indirection",
31 | "HasLocation",
32 | "HasMembers",
33 | "VariableSemantic",
34 | "Variable",
35 | "FunctionSemantic",
36 | "Function",
37 | "ClassSemantic",
38 | "Class",
39 | "Module",
40 | "load_module",
41 | "load_modules",
42 | "dump_module",
43 | "filter_visit",
44 | "visit",
45 | "get_member",
46 | ]
47 |
48 | import dataclasses
49 | import enum
50 | import io
51 | import json
52 | import sys
53 | import typing as t
54 | import weakref
55 |
56 | import databind.json
57 | import typing_extensions as te
58 | from databind.core.settings import Alias, SerializeDefaults, Union
59 |
60 |
61 | @dataclasses.dataclass
62 | class Location:
63 | """
64 | Represents the location of an #ApiObject by a filename and line number.
65 | """
66 |
67 | filename: str
68 | lineno: int
69 |
70 | #: If the location of an entity spans over multiple lines, it can be indicated by specifying at
71 | #: which line it ends with this property.
72 | endlineno: t.Optional[int] = None
73 |
74 |
75 | @dataclasses.dataclass
76 | class HasLocation:
77 | """Base class for objects that have a #Location."""
78 |
79 | location: Location
80 |
81 |
82 | @dataclasses.dataclass
83 | class Docstring(HasLocation):
84 | """
85 | Represents a docstring for an #APIObject, i.e. it's content and location. This class is a subclass of `str`
86 | for backwards compatibility reasons. Use the #content property to access the docstring content over the
87 | #Docstring value directory.
88 | """
89 |
90 | #: The location of where the docstring is defined.
91 | location: Location = dataclasses.field(repr=False)
92 |
93 | #: The content of the docstring. While the #Docstring class is a subclass of `str` and holds
94 | #: the same value as *content*, using the #content property should be preferred as the inheritance
95 | #: from the `str` class may be removed in future versions.
96 | content: str
97 |
98 |
99 | @dataclasses.dataclass
100 | class Decoration(HasLocation):
101 | """
102 | Represents a decorator on a #Class or #Function.
103 | """
104 |
105 | #: The location of the decoration in the source code.
106 | location: Location = dataclasses.field(repr=False)
107 |
108 | #: The name of the decorator (i.e. the text between the `@` and `(`). In languages that support it,
109 | #: this may be a piece of code.
110 | name: str
111 |
112 | #: Decorator arguments as plain code (including the leading and trailing parentheses). This is
113 | #: `None` when the decorator does not have call arguments. This is deprecated in favor of #arglist.
114 | #: For backwards compatibility, loaders may populate both the #args and #arglist fields.
115 | args: t.Optional[str] = None
116 |
117 | #: Decorator arguments, one item per argument. For keyword arguments, the keyword name and equals
118 | #: sign preceed the argument value expression code.
119 | arglist: t.Optional[t.List[str]] = None
120 |
121 |
122 | @dataclasses.dataclass
123 | class Argument(HasLocation):
124 | """
125 | Represents a #Function argument.
126 | """
127 |
128 | class Type(enum.Enum):
129 | """
130 | The type of the argument. This is currently very Python-centric, however most other languages should be able
131 | to represent the various argument types with a subset of these types without additions (e.g. Java or TypeScript
132 | only support #Positional and #PositionalRemainder arguments).
133 | """
134 |
135 | #: A positional only argument. Such arguments are denoted in Python like this: `def foo(a, b, /): ...`
136 | POSITIONAL_ONLY: te.Annotated[int, Alias("POSITIONAL_ONLY", "PositionalOnly")] = 0
137 |
138 | #: A positional argument, which may also be given as a keyword argument. Basically that is just a normal
139 | #: argument as you would see most commonly in Python function definitions.
140 | POSITIONAL: te.Annotated[int, Alias("POSITIONAL", "Positional")] = 1
141 |
142 | #: An argument that denotes the capture of additional positional arguments, aka. "args" or "varags".
143 | POSITIONAL_REMAINDER: te.Annotated[int, Alias("POSITIONAL_REMAINDER", "PositionalRemainder")] = 2
144 |
145 | #: A keyword-only argument is denoted in Python like thisL `def foo(*, kwonly): ...`
146 | KEYWORD_ONLY: te.Annotated[int, Alias("KEYWORD_ONLY", "KeywordOnly")] = 3
147 |
148 | #: An argument that captures additional keyword arguments, aka. "kwargs".
149 | KEYWORD_REMAINDER: te.Annotated[int, Alias("KEYWORD_REMAINDER", "KeywordRemainder")] = 4
150 |
151 | # backwards compatibility, < 1.2.0
152 | PositionalOnly: t.ClassVar["Argument.Type"]
153 | Positional: t.ClassVar["Argument.Type"]
154 | PositionalRemainder: t.ClassVar["Argument.Type"]
155 | KeywordOnly: t.ClassVar["Argument.Type"]
156 | KeywordRemainder: t.ClassVar["Argument.Type"]
157 |
158 | Type.PositionalOnly = Type.POSITIONAL_ONLY
159 | Type.Positional = Type.POSITIONAL
160 | Type.PositionalRemainder = Type.POSITIONAL_REMAINDER
161 | Type.KeywordOnly = Type.KEYWORD_ONLY
162 | Type.KeywordRemainder = Type.KEYWORD_REMAINDER
163 |
164 | #: The location of the argument in the source code.
165 | location: Location = dataclasses.field(repr=False)
166 |
167 | #: The name of the argument.
168 | name: str
169 |
170 | #: The argument type.
171 | type: Type
172 |
173 | #: A list of argument decorations. Python does not actually support decorators on function arguments
174 | #: like for example Java does. This is probably premature to add into the API, but hey, here it is.
175 | decorations: t.Optional[t.List[Decoration]] = None
176 |
177 | #: The datatype/type annotation of this argument as a code string.
178 | datatype: t.Optional[str] = None
179 |
180 | #: The default value of the argument as a code string.
181 | default_value: t.Optional[str] = None
182 |
183 |
184 | @dataclasses.dataclass
185 | class ApiObject(HasLocation):
186 | """
187 | The base class for representing "API Objects". Any API object is any addressable entity in code,
188 | be that a variable/constant, function, class or module.
189 | """
190 |
191 | #: The location of the API object, i.e. where it is sourced from/defined in the code.
192 | location: Location = dataclasses.field(repr=False)
193 |
194 | #: The name of the entity. This is usually relative to the respective parent of the entity,
195 | #: as opposed to it's fully qualified name/absolute name. However, that is more of a
196 | #: recommendation than rule. For example the #docspec_python loader by default returns
197 | #: #Module objects with their full module name (and does not create a module hierarchy).
198 | name: str
199 |
200 | #: The documentation string of the API object.
201 | docstring: t.Optional[Docstring] = dataclasses.field(repr=False)
202 |
203 | def __post_init__(self) -> None:
204 | self._parent: t.Optional["weakref.ReferenceType[HasMembers]"] = None
205 |
206 | @property
207 | def parent(self) -> t.Optional["HasMembers"]:
208 | """
209 | Returns the parent of the #HasMembers. Note that if you make any modifications to the API object tree,
210 | you will need to call #sync_hierarchy() afterwards because adding to #Class.members or #Module.members
211 | does not automatically keep the #parent property in sync.
212 | """
213 |
214 | if self._parent is not None:
215 | parent = self._parent()
216 | if parent is None:
217 | raise RuntimeError("lost reference to parent object")
218 | else:
219 | parent = None
220 | return parent
221 |
222 | @parent.setter
223 | def parent(self, parent: t.Optional["HasMembers"]) -> None:
224 | if parent is not None:
225 | self._parent = weakref.ref(parent)
226 | else:
227 | self._parent = None
228 |
229 | @property
230 | def path(self) -> t.List["ApiObject"]:
231 | """
232 | Returns a list of all of this API object's parents, from top to bottom. The list includes *self* as the
233 | last item.
234 | """
235 |
236 | result = []
237 | current: t.Optional[ApiObject] = self
238 | while current:
239 | result.append(current)
240 | current = current.parent
241 | result.reverse()
242 | return result
243 |
244 | def sync_hierarchy(self, parent: t.Optional["HasMembers"] = None) -> None:
245 | """
246 | Synchronize the hierarchy of this API object and all of it's children. This should be called when the
247 | #HasMembers.members are updated to ensure that all child objects reference the right #parent. Loaders
248 | are expected to return #ApiObject#s in a fully synchronized state such that the user does not have to
249 | call this method unless they are doing modifications to the tree.
250 | """
251 |
252 | self.parent = parent
253 |
254 |
255 | class VariableSemantic(enum.Enum):
256 | """
257 | A list of well-known properties and behaviour that can be attributed to a variable/constant.
258 | """
259 |
260 | #: The #Variable object is an instance variable of a class.
261 | INSTANCE_VARIABLE = 0
262 |
263 | #: The #Variable object is a static variable of a class.
264 | CLASS_VARIABLE = 1
265 |
266 | #: The #Variable object represents a constant value.
267 | CONSTANT = 2
268 |
269 |
270 | @dataclasses.dataclass
271 | class Variable(ApiObject):
272 | """
273 | Represents a variable assignment (e.g. for global variables (often used as constants) or class members).
274 | """
275 |
276 | Semantic: t.ClassVar = VariableSemantic
277 |
278 | #: The datatype associated with the assignment as code.
279 | datatype: t.Optional[str] = None
280 |
281 | #: The value of the variable as code.
282 | value: t.Optional[str] = None
283 |
284 | #: A list of language-specific modifiers that were used to declare this #Variable object.
285 | modifiers: t.List[str] = dataclasses.field(default_factory=list)
286 |
287 | #: A list of hints that express semantics of this #Variable object which are not otherwise
288 | #: derivable from the context.
289 | semantic_hints: t.List[VariableSemantic] = dataclasses.field(default_factory=list)
290 |
291 |
292 | @dataclasses.dataclass
293 | class Indirection(ApiObject):
294 | """
295 | Represents an imported name. It can be used to properly find the full name target of a link written with a
296 | local name.
297 | """
298 |
299 | target: str
300 |
301 |
302 | class FunctionSemantic(enum.Enum):
303 | """
304 | A list of well-known properties and behaviour that can be attributed to a function.
305 | """
306 |
307 | #: The function is abstract.
308 | ABSTRACT = 0
309 |
310 | #: The function is final.
311 | FINAL = 1
312 |
313 | #: The function is a coroutine.
314 | COROUTINE = 2
315 |
316 | #: The function does not return.
317 | NO_RETURN = 3
318 |
319 | #: The function is an instance method.
320 | INSTANCE_METHOD = 4
321 |
322 | #: The function is a classmethod.
323 | CLASS_METHOD = 5
324 |
325 | #: The function is a staticmethod.
326 | STATIC_METHOD = 6
327 |
328 | #: The function is a property getter.
329 | PROPERTY_GETTER = 7
330 |
331 | #: The function is a property setter.
332 | PROPERTY_SETTER = 8
333 |
334 | #: The function is a property deleter.
335 | PROPERTY_DELETER = 9
336 |
337 |
338 | @dataclasses.dataclass
339 | class Function(ApiObject):
340 | """
341 | Represents a function definition. This can be in a #Module for plain functions or in a #Class for methods.
342 | The #decorations need to be introspected to understand if the function has a special purpose (e.g. is it a
343 | `@property`, `@classmethod` or `@staticmethod`?).
344 | """
345 |
346 | Semantic: t.ClassVar = FunctionSemantic
347 |
348 | #: A list of modifiers used in the function definition. For example, the only valid modifier in
349 | #: Python is "async".
350 | modifiers: t.Optional[t.List[str]]
351 |
352 | #: A list of the function arguments.
353 | args: t.List[Argument]
354 |
355 | #: The return type of the function as a code string.
356 | return_type: t.Optional[str]
357 |
358 | #: A list of decorations used on the function.
359 | decorations: t.Optional[t.List[Decoration]]
360 |
361 | #: A list of hints that describe the object.
362 | semantic_hints: t.List[FunctionSemantic] = dataclasses.field(default_factory=list)
363 |
364 |
365 | @dataclasses.dataclass
366 | class HasMembers(ApiObject):
367 | """
368 | Base class for API objects that can have members, e.g. #Class and #Module.
369 | """
370 |
371 | #: The members of the API object.
372 | members: t.Sequence[ApiObject]
373 |
374 | def sync_hierarchy(self, parent: t.Optional["HasMembers"] = None) -> None:
375 | self.parent = parent
376 | for member in self.members:
377 | member.sync_hierarchy(self)
378 |
379 |
380 | class ClassSemantic(enum.Enum):
381 | """
382 | A list of well-known properties and behaviour that can be attributed to a class.
383 | """
384 |
385 | #: The class describes an interface.
386 | INTERFACE = 0
387 |
388 | #: The class is abstract.
389 | ABSTRACT = 1
390 |
391 | #: The class is final.
392 | FINAL = 2
393 |
394 | #: The class is an enumeration.
395 | ENUM = 3
396 |
397 |
398 | @dataclasses.dataclass
399 | class Class(HasMembers):
400 | """
401 | Represents a class definition.
402 | """
403 |
404 | Semantic: t.ClassVar = ClassSemantic
405 |
406 | #: The metaclass used in the class definition as a code string.
407 | metaclass: t.Optional[str]
408 |
409 | #: The list of base classes as code strings.
410 | bases: t.Optional[t.List[str]]
411 |
412 | #: A list of decorations used in the class definition.
413 | decorations: t.Optional[t.List[Decoration]]
414 |
415 | #: A list of the classes members. #Function#s in a class are to be considered instance methods of
416 | #: that class unless some information about the #Function indicates otherwise.
417 | members: t.List["_MemberType"]
418 |
419 | #: A list of language-specific modifiers that were used to declare this #Variable object.
420 | modifiers: t.List[str] = dataclasses.field(default_factory=list)
421 |
422 | #: A list of hints that describe the object.
423 | semantic_hints: t.List[ClassSemantic] = dataclasses.field(default_factory=list)
424 |
425 |
426 | @dataclasses.dataclass
427 | class Module(HasMembers):
428 | """
429 | Represents a module, basically a named container for code/API objects. Modules may be nested in other modules.
430 | Be aware that for historical reasons, some loaders lile #docspec_python by default do not return nested modules,
431 | even if nesting would be appropriate (and instead the #Module.name simply contains the fully qualified name).
432 | """
433 |
434 | #: A list of module members.
435 | members: t.List["_ModuleMemberType"]
436 |
437 |
438 | _Members = t.Union[Variable, Function, Class, Indirection]
439 | _MemberType = te.Annotated[
440 | _Members,
441 | Union({"data": Variable, "function": Function, "class": Class, "indirection": Indirection}, style=Union.FLAT),
442 | ]
443 |
444 |
445 | _ModuleMembers = t.Union[Variable, Function, Class, Module, Indirection]
446 | _ModuleMemberType = te.Annotated[
447 | _ModuleMembers,
448 | Union(
449 | {"data": Variable, "function": Function, "class": Class, "module": Module, "indirection": Indirection},
450 | style=Union.FLAT,
451 | ),
452 | ]
453 |
454 |
455 | def load_module(
456 | source: t.Union[str, t.TextIO, t.Dict[str, t.Any]],
457 | filename: t.Optional[str] = None,
458 | loader: t.Callable[[t.IO[str]], t.Any] = json.load,
459 | ) -> Module:
460 | """
461 | Loads a #Module from the specified *source*, which may be either a filename,
462 | a file-like object to read from or plain structured data.
463 |
464 | # Arguments
465 | source: The JSON source to load the module from.
466 | filename: The name of the source. This will be displayed in error
467 | messages if the deserialization fails.
468 | loader: A function for loading plain structured data from a file-like
469 | object. Defaults to #json.load().
470 |
471 | # Returns
472 | The loaded `Module` object.
473 | """
474 |
475 | filename = filename or getattr(source, "name", None)
476 |
477 | if isinstance(source, str):
478 | if source == "-":
479 | return load_module(sys.stdin, source, loader)
480 | with io.open(source, encoding="utf-8") as fp:
481 | return load_module(fp, source, loader)
482 | elif hasattr(source, "read"):
483 | # we ar sure the type is "IO" since the source has a read attribute.
484 | source = loader(source) # type: ignore[arg-type]
485 |
486 | module = databind.json.load(source, Module, filename=filename or "")
487 | module.sync_hierarchy()
488 | return module
489 |
490 |
491 | def load_modules(
492 | source: t.Union[str, t.TextIO, t.Iterable[t.Any]],
493 | filename: t.Optional[str] = None,
494 | loader: t.Callable[[t.IO[str]], t.Any] = json.load,
495 | ) -> t.Iterable[Module]:
496 | """
497 | Loads a stream of modules from the specified *source*. Similar to
498 | #load_module(), the *source* can be a filename, file-like object or a
499 | list of plain structured data to deserialize from.
500 | """
501 |
502 | filename = filename or getattr(source, "name", None)
503 |
504 | if isinstance(source, str):
505 | with io.open(source, encoding="utf-8") as fp:
506 | yield from load_modules(fp, source, loader)
507 | return
508 | elif hasattr(source, "read"):
509 | source = (loader(io.StringIO(line)) for line in t.cast(t.IO[str], source))
510 |
511 | for data in source:
512 | module = databind.json.load(data, Module, filename=filename or "")
513 | module.sync_hierarchy()
514 | yield module
515 |
516 |
517 | @t.overload
518 | def dump_module(
519 | module: Module, target: t.Union[str, t.IO[str]], dumper: t.Callable[[t.Any, t.IO[str]], None] = json.dump
520 | ) -> None: ...
521 |
522 |
523 | @t.overload
524 | def dump_module(
525 | module: Module, target: None = None, dumper: t.Callable[[t.Any, t.IO[str]], None] = json.dump
526 | ) -> t.Dict[str, t.Any]: ...
527 |
528 |
529 | def dump_module(
530 | module: Module,
531 | target: t.Optional[t.Union[str, t.IO[str]]] = None,
532 | dumper: t.Callable[[t.Any, t.IO[str]], None] = json.dump,
533 | serialize_defaults: bool = False,
534 | ) -> t.Optional[t.Dict[str, t.Any]]:
535 | """
536 | Dumps a module to the specified target or returns it as plain structured data.
537 |
538 | :param module: The module to dump.
539 | :param target: The target to dump to. If #None, the module will be returned as plain structured data.
540 | :param dumper: A function for dumping plain structured data to a file-like object. Defaults to #json.dump().
541 | :param serialize_defaults: If #True, default values will be serialized into the payload. Otherwise, they will be
542 | omitted. Defaults to #False.
543 | """
544 |
545 | if isinstance(target, str):
546 | with io.open(target, "w", encoding="utf-8") as fp:
547 | dump_module(module, fp, dumper)
548 | return None
549 |
550 | data = databind.json.dump(module, Module, settings=[SerializeDefaults(serialize_defaults)])
551 | if target:
552 | dumper(data, target)
553 | target.write("\n")
554 | return None
555 | else:
556 | return t.cast(t.Dict[str, t.Any], data)
557 |
558 |
559 | def filter_visit(
560 | objects: t.MutableSequence[ApiObject],
561 | predicate: t.Callable[[ApiObject], bool],
562 | order: str = "pre",
563 | ) -> t.MutableSequence[ApiObject]:
564 | """
565 | Visits all *objects* recursively, applying the *predicate* in the specified *order*. If
566 | the predicate returrns #False, the object will be removed from it's containing list.
567 |
568 | If an object is removed in pre-order, it's members will not be visited.
569 |
570 | :param objects: A list of objects to visit recursively. This list will be modified if
571 | the *predicate* returns #False for an object.
572 | :param predicate: The function to apply over all visited objects.
573 | :param order: The order in which the objects are visited. The default order is `'pre'`
574 | in which case the *predicate* is called before visiting the object's members. The
575 | order may also be `'post'`.
576 | """
577 |
578 | if order not in ("pre", "post"):
579 | raise ValueError("invalid order: {!r}".format(order))
580 |
581 | offset = 0
582 | for index in range(len(objects)):
583 | current = objects[index - offset]
584 | if order == "pre":
585 | if not predicate(current):
586 | del objects[index - offset]
587 | offset += 1
588 | continue
589 | if isinstance(current, HasMembers):
590 | current.members = filter_visit(list(current.members), predicate, order)
591 | if order == "post":
592 | if not predicate(current):
593 | del objects[index - offset]
594 | offset += 1
595 |
596 | return objects
597 |
598 |
599 | def visit(
600 | objects: t.Sequence[ApiObject],
601 | func: t.Callable[[ApiObject], t.Any],
602 | order: str = "pre",
603 | ) -> None:
604 | """
605 | Visits all *objects*, applying *func* in the specified *order*.
606 | """
607 |
608 | filter_visit(
609 | t.cast(t.MutableSequence[ApiObject], objects), # Sequence does not get mutated in this call
610 | (lambda obj: func(obj) or True),
611 | order,
612 | )
613 |
614 |
615 | def get_member(obj: ApiObject, name: str) -> t.Optional[ApiObject]:
616 | """
617 | Generic function to retrieve a member from an API object. This will always return #None for
618 | objects that don't support members (eg. #Function and #Variable).
619 | """
620 |
621 | if isinstance(obj, HasMembers):
622 | for member in obj.members:
623 | if member.name == name:
624 | assert isinstance(member, ApiObject), (name, obj, member)
625 | return member
626 |
627 | return None
628 |
--------------------------------------------------------------------------------
/docspec/src/docspec/__main__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf8 -*-
2 | # Copyright (c) 2021 Niklas Rosenstein
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to
6 | # deal in the Software without restriction, including without limitation the
7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8 | # sell copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in
12 | # all copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20 | # IN THE SOFTWARE.
21 |
22 | import argparse
23 | import sys
24 |
25 | import docspec
26 |
27 | try:
28 | from termcolor import colored
29 | except ImportError:
30 |
31 | def colored(s, *args, **kwargs): # type: ignore
32 | return str(s)
33 |
34 |
35 | _COLOR_MAP = {
36 | docspec.Module: "magenta",
37 | docspec.Class: "cyan",
38 | docspec.Function: "yellow",
39 | docspec.Variable: "blue",
40 | }
41 |
42 |
43 | def _dump_tree(obj: docspec.ApiObject, depth: int = 0) -> None:
44 | color = _COLOR_MAP.get(type(obj))
45 | type_name = colored(type(obj).__name__.lower(), color)
46 | print("| " * depth + type_name, obj.name)
47 | for member in getattr(obj, "members", []):
48 | _dump_tree(member, depth + 1)
49 |
50 |
51 | def main() -> None:
52 | parser = argparse.ArgumentParser()
53 | parser.add_argument("file", nargs="?")
54 | parser.add_argument("-t", "--tty", action="store_true", help="Read from stdin even if it is a TTY.")
55 | parser.add_argument("-m", "--multiple", action="store_true", help="Load a module per line from the input.")
56 | parser.add_argument(
57 | "--dump-tree",
58 | action="store_true",
59 | help="Dump a simplified tree representation of the parsed module(s) to stdout. Supports colored output if the "
60 | '"termcolor" module is installed.',
61 | )
62 | args = parser.parse_args()
63 |
64 | if not args.file and sys.stdin.isatty() and not args.tty:
65 | parser.print_usage()
66 | sys.exit(1)
67 |
68 | if args.multiple:
69 | modules = docspec.load_modules(args.file or sys.stdin)
70 | else:
71 | modules = [docspec.load_module(args.file or sys.stdin)]
72 |
73 | if args.dump_tree:
74 | for module in modules:
75 | _dump_tree(module)
76 | else:
77 | for module in modules:
78 | docspec.dump_module(module, sys.stdout)
79 |
80 |
81 | if __name__ == "__main__":
82 | main()
83 |
--------------------------------------------------------------------------------
/docspec/src/docspec/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NiklasRosenstein/python-docspec/61d3e38c55d290da6197236357e1cdfe818d35b5/docspec/src/docspec/py.typed
--------------------------------------------------------------------------------
/docspec/test/__init__.py:
--------------------------------------------------------------------------------
1 | pass
2 |
--------------------------------------------------------------------------------
/docspec/test/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import docspec
4 |
5 | loc = docspec.Location("", 0, None)
6 |
7 |
8 | @pytest.fixture
9 | def module() -> docspec.Module:
10 | module = docspec.Module(
11 | loc,
12 | "a",
13 | None,
14 | [
15 | docspec.Class(
16 | loc,
17 | "foo",
18 | docspec.Docstring(loc, "This is class foo."),
19 | [
20 | docspec.Variable(loc, "val", None, "int", "42"),
21 | docspec.Function(
22 | loc,
23 | "__init__",
24 | None,
25 | None,
26 | [docspec.Argument(loc, "self", docspec.Argument.Type.POSITIONAL)],
27 | None,
28 | None,
29 | ),
30 | ],
31 | None,
32 | None,
33 | None,
34 | ),
35 | ],
36 | )
37 | module.sync_hierarchy()
38 | return module
39 |
40 |
41 | @pytest.fixture
42 | def typed_module() -> docspec.Module:
43 | module = docspec.Module(
44 | docspec.Location("test.py", 0),
45 | "a",
46 | None,
47 | [
48 | docspec.Indirection(docspec.Location("test.py", 1), "Union", None, "typing.Union"),
49 | docspec.Class(
50 | docspec.Location("test.py", 2),
51 | "foo",
52 | docspec.Docstring(docspec.Location("test.py", 3), "This is class foo."),
53 | [
54 | docspec.Variable(docspec.Location("test.py", 4), "val", None, "Union[int, float]", "42"),
55 | docspec.Function(
56 | docspec.Location("test.py", 5),
57 | "__init__",
58 | None,
59 | None,
60 | [docspec.Argument(docspec.Location("test.py", 5), "self", docspec.Argument.Type.POSITIONAL)],
61 | None,
62 | None,
63 | ),
64 | ],
65 | None,
66 | None,
67 | [],
68 | ),
69 | ],
70 | )
71 | module.sync_hierarchy()
72 | return module
73 |
--------------------------------------------------------------------------------
/docspec/test/docspec/__init__.py:
--------------------------------------------------------------------------------
1 | pass
2 |
--------------------------------------------------------------------------------
/docspec/test/docspec/test_deserialize.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import typing as t
4 | import weakref
5 |
6 | import docspec
7 |
8 | loc = docspec.Location("", 0)
9 | s_loc = {"filename": "", "lineno": 0}
10 |
11 |
12 | def test_serialize_typed(typed_module: docspec.Module) -> None:
13 | assert docspec.dump_module(typed_module) == {
14 | "location": {"filename": "test.py", "lineno": 0},
15 | "members": [
16 | {
17 | "location": {"filename": "test.py", "lineno": 1},
18 | "name": "Union",
19 | "target": "typing.Union",
20 | "type": "indirection",
21 | },
22 | {
23 | "docstring": {"content": "This is class foo.", "location": {"filename": "test.py", "lineno": 3}},
24 | "location": {"filename": "test.py", "lineno": 2},
25 | "members": [
26 | {
27 | "datatype": "Union[int, float]",
28 | "location": {"filename": "test.py", "lineno": 4},
29 | "name": "val",
30 | "type": "data",
31 | "value": "42",
32 | },
33 | {
34 | "args": [
35 | {"name": "self", "type": "POSITIONAL", "location": {"filename": "test.py", "lineno": 5}}
36 | ],
37 | "location": {"filename": "test.py", "lineno": 5},
38 | "name": "__init__",
39 | "type": "function",
40 | },
41 | ],
42 | "name": "foo",
43 | "type": "class",
44 | "decorations": [],
45 | },
46 | ],
47 | "name": "a",
48 | }
49 |
50 |
51 | def test_serialize(module: docspec.Module) -> None:
52 | assert docspec.dump_module(module) == {
53 | "name": "a",
54 | "location": s_loc,
55 | "members": [
56 | {
57 | "type": "class",
58 | "name": "foo",
59 | "location": s_loc,
60 | "docstring": {
61 | "content": "This is class foo.",
62 | "location": s_loc,
63 | },
64 | "members": [
65 | {
66 | "type": "data",
67 | "name": "val",
68 | "location": s_loc,
69 | "datatype": "int",
70 | "value": "42",
71 | },
72 | {
73 | "type": "function",
74 | "name": "__init__",
75 | "location": s_loc,
76 | "args": [
77 | {
78 | "location": s_loc,
79 | "name": "self",
80 | "type": "POSITIONAL",
81 | }
82 | ],
83 | },
84 | ],
85 | }
86 | ],
87 | }
88 |
89 |
90 | def test_serialize_deserialize(module: docspec.Module) -> None:
91 | deser = docspec.load_module(docspec.dump_module(module))
92 | assert deser == module
93 |
94 | def _deep_comparison(a: t.Any, b: t.Any, path: list[str | int], seen: set[int]) -> None:
95 | assert isinstance(a, type(b)), path
96 | if isinstance(a, weakref.ref):
97 | a, b = a(), b()
98 | assert a == b, path
99 | assert a == b, path
100 | if id(a) in seen and id(b) in seen:
101 | return
102 | seen.update({id(a), id(b)})
103 | if hasattr(a, "__dict__"):
104 | a, b = vars(a), vars(b)
105 | if isinstance(a, t.Mapping):
106 | for key in a:
107 | _deep_comparison(a[key], b[key], path + [key], seen)
108 | elif isinstance(a, t.Sequence) and not isinstance(a, (bytes, str)):
109 | for i in range(len(a)):
110 | _deep_comparison(a[i], b[i], path + [i], seen)
111 |
112 | _deep_comparison(deser, module, ["$"], set())
113 |
114 |
115 | def test_deserialize_old_function_argument_types() -> None:
116 | payload = {
117 | "name": "a",
118 | "location": s_loc,
119 | "docstring": None,
120 | "members": [
121 | {
122 | "type": "function",
123 | "name": "bar",
124 | "location": s_loc,
125 | "docstring": None,
126 | "modifiers": None,
127 | "return_type": None,
128 | "decorations": None,
129 | "args": [{"location": s_loc, "name": "n", "datatype": "int", "type": "Positional"}],
130 | }
131 | ],
132 | }
133 | assert docspec.load_module(payload) == docspec.Module(
134 | name="a",
135 | location=loc,
136 | docstring=None,
137 | members=[
138 | docspec.Function(
139 | name="bar",
140 | location=loc,
141 | docstring=None,
142 | modifiers=None,
143 | return_type=None,
144 | decorations=None,
145 | args=[docspec.Argument(location=loc, name="n", datatype="int", type=docspec.Argument.Type.POSITIONAL)],
146 | )
147 | ],
148 | )
149 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1731533236,
9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1733015953,
24 | "narHash": "sha256-t4BBVpwG9B4hLgc6GUBuj3cjU7lP/PJfpTHuSqE+crk=",
25 | "owner": "nixos",
26 | "repo": "nixpkgs",
27 | "rev": "ac35b104800bff9028425fec3b6e8a41de2bbfff",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "nixos",
32 | "ref": "nixos-unstable",
33 | "repo": "nixpkgs",
34 | "type": "github"
35 | }
36 | },
37 | "root": {
38 | "inputs": {
39 | "flake-utils": "flake-utils",
40 | "nixpkgs": "nixpkgs"
41 | }
42 | },
43 | "systems": {
44 | "locked": {
45 | "lastModified": 1681028828,
46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47 | "owner": "nix-systems",
48 | "repo": "default",
49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "nix-systems",
54 | "repo": "default",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | inputs = {
3 | nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
4 | flake-utils.url = "github:numtide/flake-utils";
5 | };
6 | outputs = { self, nixpkgs, flake-utils }:
7 | flake-utils.lib.eachDefaultSystem (system:
8 | let pkgs = import nixpkgs { inherit system; };
9 | in {
10 | packages.lint = pkgs.writeShellScriptBin "lint" ''
11 | set -e
12 | ${pkgs.ruff}/bin/ruff check .
13 | ( cd docspec/ && ${pkgs.uv}/bin/uv run mypy . --check-untyped-defs )
14 | ( cd docspec-python/ && ${pkgs.uv}/bin/uv run mypy . --check-untyped-defs )
15 | '';
16 |
17 | packages.test = pkgs.writeShellScriptBin "test" ''
18 | set -e
19 | ( cd docspec/ && ${pkgs.uv}/bin/uv run pytest . )
20 | ( cd docspec-python/ && ${pkgs.uv}/bin/uv run pytest . )
21 | '';
22 |
23 | packages.docs = let
24 | slap = pkgs.writeShellScriptBin "slap" ''
25 | ${pkgs.uv}/bin/uv tool run --from slap-cli slap -- "$@"
26 | '';
27 | in pkgs.writeShellScriptBin "docs" ''
28 | set -e
29 | export PATH="${slap}/bin:$PATH"
30 | ( cd docs/ && ${pkgs.uv}/bin/uv run novella --base-url docspec/ "$@" )
31 | '';
32 |
33 | formatter = pkgs.writeShellScriptBin "ruff" ''
34 | set -e
35 | ${pkgs.nixfmt-classic}/bin/nixfmt .
36 | ${pkgs.ruff}/bin/ruff format .
37 | '';
38 |
39 | devShells.default = pkgs.mkShell { nativeBuildInputs = [ pkgs.uv ]; };
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.uv.sources]
2 | docspec = { workspace = true }
3 | docspec-python = { workspace = true }
4 |
5 | [tool.uv.workspace]
6 | members = ["docspec/", "docspec-python/", "docs"]
7 | exclude = ["docs"]
8 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | > Note: The GitHub repository was moved from NiklasRosenstein/docspec to NiklasRosenstein/python-docspec on
2 | > May 13, 2023.
3 |
4 | # Docspec
5 |
6 | Docspec is a JSON object specification for representing API documentation of programming languages. While in
7 | it's current form it is targeting Python APIs, it is intended to be able to represent other programming
8 | languages in the future as well
9 |
10 | ## What is...?
11 |
12 | ### docspec
13 |
14 | The reference implementation for reading/writing the JSON format and API for representing API objects in memory.
15 |
16 | [→ View the Specification 📃](https://niklasrosenstein.github.io/python-docspec/specification/)
17 |
18 | ### docspec-python
19 |
20 | A parser for Python packages and modules based on `lib2to3` producing `docspec` API object representations.
21 |
22 | [→ View the Documentation 📘](https://niklasrosenstein.github.io/python-docspec/api/docspec-python/)
23 |
24 | ## Projects using `docspec`
25 |
26 | * [Pydoc-Markdown](https://github.com/NiklasRosenstein/pydoc-markdown) – The original spark for Docspec.
27 |
28 | ---
29 |
30 | Copyright © 2021, Niklas Rosenstein
31 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | requires-python = ">=3.8"
3 |
4 | [manifest]
5 | members = [
6 | "docspec",
7 | "docspec-python",
8 | ]
9 |
10 | [[package]]
11 | name = "black"
12 | version = "24.8.0"
13 | source = { registry = "https://pypi.org/simple" }
14 | dependencies = [
15 | { name = "click" },
16 | { name = "mypy-extensions" },
17 | { name = "packaging" },
18 | { name = "pathspec" },
19 | { name = "platformdirs" },
20 | { name = "tomli", marker = "python_full_version < '3.11'" },
21 | { name = "typing-extensions", marker = "python_full_version < '3.11'" },
22 | ]
23 | sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810 }
24 | wheels = [
25 | { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092 },
26 | { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529 },
27 | { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443 },
28 | { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012 },
29 | { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080 },
30 | { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143 },
31 | { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774 },
32 | { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503 },
33 | { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132 },
34 | { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665 },
35 | { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458 },
36 | { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109 },
37 | { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322 },
38 | { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108 },
39 | { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786 },
40 | { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754 },
41 | { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706 },
42 | { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429 },
43 | { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488 },
44 | { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721 },
45 | { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504 },
46 | ]
47 |
48 | [[package]]
49 | name = "click"
50 | version = "8.1.7"
51 | source = { registry = "https://pypi.org/simple" }
52 | dependencies = [
53 | { name = "colorama", marker = "platform_system == 'Windows'" },
54 | ]
55 | sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
56 | wheels = [
57 | { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
58 | ]
59 |
60 | [[package]]
61 | name = "colorama"
62 | version = "0.4.6"
63 | source = { registry = "https://pypi.org/simple" }
64 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
65 | wheels = [
66 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
67 | ]
68 |
69 | [[package]]
70 | name = "databind-core"
71 | version = "4.4.2"
72 | source = { registry = "https://pypi.org/simple" }
73 | dependencies = [
74 | { name = "deprecated" },
75 | { name = "nr-date" },
76 | { name = "nr-stream" },
77 | { name = "setuptools", marker = "python_full_version < '3.10'" },
78 | { name = "typeapi" },
79 | { name = "typing-extensions" },
80 | ]
81 | sdist = { url = "https://files.pythonhosted.org/packages/a7/f1/cc4873e6e6475cbae0b49dcd88bc17154025d46083c247b555407c8ee08a/databind.core-4.4.2.tar.gz", hash = "sha256:4dcc0106a54e597d4d583f4b5c311b310231dd779c1d2c6cea3ffe3553c8108c", size = 26609 }
82 | wheels = [
83 | { url = "https://files.pythonhosted.org/packages/4a/4a/f5a3e83040571706fdc1083ed4615ce1687544f19783d8c92d523d782702/databind.core-4.4.2-py3-none-any.whl", hash = "sha256:9c41be88ec5902dde0c08f81cf021d417b2b7e4803df45778f73fa6fab75eb7c", size = 31511 },
84 | ]
85 |
86 | [[package]]
87 | name = "databind-json"
88 | version = "4.4.2"
89 | source = { registry = "https://pypi.org/simple" }
90 | dependencies = [
91 | { name = "databind-core" },
92 | { name = "nr-date" },
93 | { name = "typeapi" },
94 | { name = "typing-extensions" },
95 | ]
96 | sdist = { url = "https://files.pythonhosted.org/packages/d5/ce/1e82e8ba89a7c13d4c61ac88342c2619ef0b70de0b1cc4e91c43c7540e6a/databind.json-4.4.2.tar.gz", hash = "sha256:ef81a3b1f57262e07ac042a8f32427e907cbd740cc4362f805d865751a31a8d2", size = 18940 }
97 | wheels = [
98 | { url = "https://files.pythonhosted.org/packages/84/2e/b5ae9fb1ed85d489cdf76827049a797c89dbb65b340ca96a141166e462d0/databind.json-4.4.2-py3-none-any.whl", hash = "sha256:9075fa575ba17bf4536c7707fd9f9f02a205f988e47eae7ccc8f94419787f36d", size = 19121 },
99 | ]
100 |
101 | [[package]]
102 | name = "deprecated"
103 | version = "1.2.15"
104 | source = { registry = "https://pypi.org/simple" }
105 | dependencies = [
106 | { name = "wrapt" },
107 | ]
108 | sdist = { url = "https://files.pythonhosted.org/packages/2e/a3/53e7d78a6850ffdd394d7048a31a6f14e44900adedf190f9a165f6b69439/deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d", size = 2977612 }
109 | wheels = [
110 | { url = "https://files.pythonhosted.org/packages/1d/8f/c7f227eb42cfeaddce3eb0c96c60cbca37797fa7b34f8e1aeadf6c5c0983/Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320", size = 9941 },
111 | ]
112 |
113 | [[package]]
114 | name = "docspec"
115 | version = "2.2.1"
116 | source = { editable = "docspec" }
117 | dependencies = [
118 | { name = "databind-core" },
119 | { name = "databind-json" },
120 | { name = "deprecated" },
121 | ]
122 |
123 | [package.dev-dependencies]
124 | dev = [
125 | { name = "mypy" },
126 | { name = "pytest" },
127 | { name = "types-deprecated" },
128 | { name = "types-termcolor" },
129 | ]
130 |
131 | [package.metadata]
132 | requires-dist = [
133 | { name = "databind-core", specifier = ">=4.2.6" },
134 | { name = "databind-json", specifier = ">=4.2.6" },
135 | { name = "deprecated", specifier = ">=1.2.12" },
136 | ]
137 |
138 | [package.metadata.requires-dev]
139 | dev = [
140 | { name = "mypy", specifier = ">=1.13.0" },
141 | { name = "pytest", specifier = ">=8.3.4" },
142 | { name = "types-deprecated", specifier = ">=1.2.15.20241117" },
143 | { name = "types-termcolor", specifier = ">=1.1.6.2" },
144 | ]
145 |
146 | [[package]]
147 | name = "docspec-python"
148 | version = "2.2.1"
149 | source = { editable = "docspec-python" }
150 | dependencies = [
151 | { name = "black" },
152 | { name = "docspec" },
153 | { name = "nr-util" },
154 | ]
155 |
156 | [package.dev-dependencies]
157 | dev = [
158 | { name = "mypy" },
159 | { name = "pytest" },
160 | { name = "types-deprecated" },
161 | { name = "types-termcolor" },
162 | ]
163 |
164 | [package.metadata]
165 | requires-dist = [
166 | { name = "black", specifier = ">=24.8.0" },
167 | { name = "docspec", editable = "docspec" },
168 | { name = "nr-util", specifier = ">=0.8.12" },
169 | ]
170 |
171 | [package.metadata.requires-dev]
172 | dev = [
173 | { name = "mypy", specifier = ">=1.13.0" },
174 | { name = "pytest", specifier = ">=8.3.4" },
175 | { name = "types-deprecated", specifier = ">=1.2.15.20241117" },
176 | { name = "types-termcolor", specifier = ">=1.1.6.2" },
177 | ]
178 |
179 | [[package]]
180 | name = "exceptiongroup"
181 | version = "1.2.2"
182 | source = { registry = "https://pypi.org/simple" }
183 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
184 | wheels = [
185 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
186 | ]
187 |
188 | [[package]]
189 | name = "iniconfig"
190 | version = "2.0.0"
191 | source = { registry = "https://pypi.org/simple" }
192 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
193 | wheels = [
194 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
195 | ]
196 |
197 | [[package]]
198 | name = "mypy"
199 | version = "1.13.0"
200 | source = { registry = "https://pypi.org/simple" }
201 | dependencies = [
202 | { name = "mypy-extensions" },
203 | { name = "tomli", marker = "python_full_version < '3.11'" },
204 | { name = "typing-extensions" },
205 | ]
206 | sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 }
207 | wheels = [
208 | { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 },
209 | { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 },
210 | { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 },
211 | { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 },
212 | { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 },
213 | { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 },
214 | { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 },
215 | { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 },
216 | { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 },
217 | { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 },
218 | { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 },
219 | { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 },
220 | { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 },
221 | { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 },
222 | { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 },
223 | { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 },
224 | { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 },
225 | { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 },
226 | { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 },
227 | { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 },
228 | { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147 },
229 | { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373 },
230 | { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621 },
231 | { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348 },
232 | { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311 },
233 | { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 },
234 | { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 },
235 | { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 },
236 | { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 },
237 | { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 },
238 | { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 },
239 | ]
240 |
241 | [[package]]
242 | name = "mypy-extensions"
243 | version = "1.0.0"
244 | source = { registry = "https://pypi.org/simple" }
245 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
246 | wheels = [
247 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
248 | ]
249 |
250 | [[package]]
251 | name = "nr-date"
252 | version = "2.1.0"
253 | source = { registry = "https://pypi.org/simple" }
254 | sdist = { url = "https://files.pythonhosted.org/packages/a0/92/08110dd3d7ff5e2b852a220752eb6c40183839f5b7cc91f9f38dd2298e7d/nr_date-2.1.0.tar.gz", hash = "sha256:0643aea13bcdc2a8bc56af9d5e6a89ef244c9744a1ef00cdc735902ba7f7d2e6", size = 8789 }
255 | wheels = [
256 | { url = "https://files.pythonhosted.org/packages/f9/10/1d2b00172537c1522fe64bbc6fb16b015632a02f7b3864e788ccbcb4dd85/nr_date-2.1.0-py3-none-any.whl", hash = "sha256:bd672a9dfbdcf7c4b9289fea6750c42490eaee08036a72059dcc78cb236ed568", size = 10496 },
257 | ]
258 |
259 | [[package]]
260 | name = "nr-stream"
261 | version = "1.1.5"
262 | source = { registry = "https://pypi.org/simple" }
263 | sdist = { url = "https://files.pythonhosted.org/packages/b7/37/e4d36d852c441233c306c5fbd98147685dce3ac9b0a8bbf4a587d0ea29ea/nr_stream-1.1.5.tar.gz", hash = "sha256:eb0216c6bfc61a46d4568dba3b588502c610ec8ddef4ac98f3932a2bd7264f65", size = 10053 }
264 | wheels = [
265 | { url = "https://files.pythonhosted.org/packages/1d/e1/f93485fe09aa36c0e1a3b76363efa1791241f7f863a010f725c95e8a74fe/nr_stream-1.1.5-py3-none-any.whl", hash = "sha256:47e12150b331ad2cb729cfd9d2abd281c9949809729ba461c6aa87dd9927b2d4", size = 10448 },
266 | ]
267 |
268 | [[package]]
269 | name = "nr-util"
270 | version = "0.8.12"
271 | source = { registry = "https://pypi.org/simple" }
272 | dependencies = [
273 | { name = "deprecated" },
274 | { name = "typing-extensions" },
275 | ]
276 | sdist = { url = "https://files.pythonhosted.org/packages/20/0c/078c567d95e25564bc1ede3c2cf6ce1c91f50648c83786354b47224326da/nr.util-0.8.12.tar.gz", hash = "sha256:a4549c2033d99d2f0379b3f3d233fd2a8ade286bbf0b3ad0cc7cea16022214f4", size = 63707 }
277 | wheels = [
278 | { url = "https://files.pythonhosted.org/packages/ba/58/eab08df9dbd69d9e21fc5e7be6f67454f386336ec71e6b64e378a2dddea4/nr.util-0.8.12-py3-none-any.whl", hash = "sha256:91da02ac9795eb8e015372275c1efe54bac9051231ee9b0e7e6f96b0b4e7d2bb", size = 90319 },
279 | ]
280 |
281 | [[package]]
282 | name = "packaging"
283 | version = "24.2"
284 | source = { registry = "https://pypi.org/simple" }
285 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
286 | wheels = [
287 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
288 | ]
289 |
290 | [[package]]
291 | name = "pathspec"
292 | version = "0.12.1"
293 | source = { registry = "https://pypi.org/simple" }
294 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
295 | wheels = [
296 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
297 | ]
298 |
299 | [[package]]
300 | name = "platformdirs"
301 | version = "4.3.6"
302 | source = { registry = "https://pypi.org/simple" }
303 | sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
304 | wheels = [
305 | { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
306 | ]
307 |
308 | [[package]]
309 | name = "pluggy"
310 | version = "1.5.0"
311 | source = { registry = "https://pypi.org/simple" }
312 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
313 | wheels = [
314 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
315 | ]
316 |
317 | [[package]]
318 | name = "pytest"
319 | version = "8.3.4"
320 | source = { registry = "https://pypi.org/simple" }
321 | dependencies = [
322 | { name = "colorama", marker = "sys_platform == 'win32'" },
323 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
324 | { name = "iniconfig" },
325 | { name = "packaging" },
326 | { name = "pluggy" },
327 | { name = "tomli", marker = "python_full_version < '3.11'" },
328 | ]
329 | sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
330 | wheels = [
331 | { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
332 | ]
333 |
334 | [[package]]
335 | name = "setuptools"
336 | version = "75.3.0"
337 | source = { registry = "https://pypi.org/simple" }
338 | sdist = { url = "https://files.pythonhosted.org/packages/ed/22/a438e0caa4576f8c383fa4d35f1cc01655a46c75be358960d815bfbb12bd/setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686", size = 1351577 }
339 | wheels = [
340 | { url = "https://files.pythonhosted.org/packages/90/12/282ee9bce8b58130cb762fbc9beabd531549952cac11fc56add11dcb7ea0/setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd", size = 1251070 },
341 | ]
342 |
343 | [[package]]
344 | name = "tomli"
345 | version = "2.2.1"
346 | source = { registry = "https://pypi.org/simple" }
347 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
348 | wheels = [
349 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
350 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
351 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
352 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
353 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
354 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
355 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
356 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
357 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
358 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
359 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
360 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
361 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
362 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
363 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
364 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
365 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
366 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
367 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
368 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
369 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
370 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
371 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
372 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
373 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
374 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
375 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
376 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
377 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
378 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
379 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
380 | ]
381 |
382 | [[package]]
383 | name = "typeapi"
384 | version = "2.2.3"
385 | source = { registry = "https://pypi.org/simple" }
386 | dependencies = [
387 | { name = "typing-extensions" },
388 | ]
389 | sdist = { url = "https://files.pythonhosted.org/packages/68/41/42befa6c08213ee6ebf9847c4eb8e658904b79895f1a365d0a5a935190ab/typeapi-2.2.3.tar.gz", hash = "sha256:61cf8c852c05471522fcf55ec37d0c37f0de6943cc8e4d58529f796881e32c08", size = 23224 }
390 | wheels = [
391 | { url = "https://files.pythonhosted.org/packages/1b/f8/7c0ef4b5f2decb27c50f6c080b9ff04a7174fbc99cb4a7ce298e0d2a6834/typeapi-2.2.3-py3-none-any.whl", hash = "sha256:038062b473dd9bc182966469d7a37d81ba7fa5bb0c01f30b0604b5667b13a47b", size = 26441 },
392 | ]
393 |
394 | [[package]]
395 | name = "types-deprecated"
396 | version = "1.2.15.20241117"
397 | source = { registry = "https://pypi.org/simple" }
398 | sdist = { url = "https://files.pythonhosted.org/packages/a8/76/d3735190891b12533115e73ac835cfdd1f28378b6b39fd50dfe2fbd63143/types-Deprecated-1.2.15.20241117.tar.gz", hash = "sha256:924002c8b7fddec51ba4949788a702411a2e3636cd9b2a33abd8ee119701d77e", size = 3377 }
399 | wheels = [
400 | { url = "https://files.pythonhosted.org/packages/27/ed/9091bd7a90d3e2e08ee8c0bdbf0c826d3d9e3730ddd9b15cb64f4ae51b9b/types_Deprecated-1.2.15.20241117-py3-none-any.whl", hash = "sha256:a0cc5e39f769fc54089fd8e005416b55d74aa03f6964d2ed1a0b0b2e28751884", size = 3779 },
401 | ]
402 |
403 | [[package]]
404 | name = "types-termcolor"
405 | version = "1.1.6.2"
406 | source = { registry = "https://pypi.org/simple" }
407 | sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/77b1d73399d1cb77823ad32a36490b6a9851a7bd84f3a54560adab7ae022/types-termcolor-1.1.6.2.tar.gz", hash = "sha256:d8f0f69cf5552cc59ce75aa5172937cec9b320c17453adefe4168b93d16daad6", size = 2594 }
408 | wheels = [
409 | { url = "https://files.pythonhosted.org/packages/d4/94/0b01039dcb59173fc1f172a29b455986108f02fad9c5e103ff48afe17f35/types_termcolor-1.1.6.2-py3-none-any.whl", hash = "sha256:44c4c762c54a90d99b5c1033ef008aaa5610056d31d5c66b9288a942682a64d7", size = 2360 },
410 | ]
411 |
412 | [[package]]
413 | name = "typing-extensions"
414 | version = "4.6.3"
415 | source = { registry = "https://pypi.org/simple" }
416 | sdist = { url = "https://files.pythonhosted.org/packages/42/56/cfaa7a5281734dadc842f3a22e50447c675a1c5a5b9f6ad8a07b467bffe7/typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5", size = 65757 }
417 | wheels = [
418 | { url = "https://files.pythonhosted.org/packages/5f/86/d9b1518d8e75b346a33eb59fa31bdbbee11459a7e2cc5be502fa779e96c5/typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", size = 31329 },
419 | ]
420 |
421 | [[package]]
422 | name = "wrapt"
423 | version = "1.17.0"
424 | source = { registry = "https://pypi.org/simple" }
425 | sdist = { url = "https://files.pythonhosted.org/packages/24/a1/fc03dca9b0432725c2e8cdbf91a349d2194cf03d8523c124faebe581de09/wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801", size = 55542 }
426 | wheels = [
427 | { url = "https://files.pythonhosted.org/packages/99/f9/85220321e9bb1a5f72ccce6604395ae75fcb463d87dad0014dc1010bd1f1/wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8", size = 38766 },
428 | { url = "https://files.pythonhosted.org/packages/ff/71/ff624ff3bde91ceb65db6952cdf8947bc0111d91bd2359343bc2fa7c57fd/wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d", size = 83262 },
429 | { url = "https://files.pythonhosted.org/packages/9f/0a/814d4a121a643af99cfe55a43e9e6dd08f4a47cdac8e8f0912c018794715/wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df", size = 74990 },
430 | { url = "https://files.pythonhosted.org/packages/cd/c7/b8c89bf5ca5c4e6a2d0565d149d549cdb4cffb8916d1d1b546b62fb79281/wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d", size = 82712 },
431 | { url = "https://files.pythonhosted.org/packages/19/7c/5977aefa8460906c1ff914fd42b11cf6c09ded5388e46e1cc6cea4ab15e9/wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea", size = 81705 },
432 | { url = "https://files.pythonhosted.org/packages/ae/e7/233402d7bd805096bb4a8ec471f5a141421a01de3c8c957cce569772c056/wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb", size = 74636 },
433 | { url = "https://files.pythonhosted.org/packages/93/81/b6c32d8387d9cfbc0134f01585dee7583315c3b46dfd3ae64d47693cd078/wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301", size = 81299 },
434 | { url = "https://files.pythonhosted.org/packages/d1/c3/1fae15d453468c98f09519076f8d401b476d18d8d94379e839eed14c4c8b/wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22", size = 36425 },
435 | { url = "https://files.pythonhosted.org/packages/c6/f4/77e0886c95556f2b4caa8908ea8eb85f713fc68296a2113f8c63d50fe0fb/wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575", size = 38748 },
436 | { url = "https://files.pythonhosted.org/packages/0e/40/def56538acddc2f764c157d565b9f989072a1d2f2a8e384324e2e104fc7d/wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a", size = 38766 },
437 | { url = "https://files.pythonhosted.org/packages/89/e2/8c299f384ae4364193724e2adad99f9504599d02a73ec9199bf3f406549d/wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed", size = 83730 },
438 | { url = "https://files.pythonhosted.org/packages/29/ef/fcdb776b12df5ea7180d065b28fa6bb27ac785dddcd7202a0b6962bbdb47/wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489", size = 75470 },
439 | { url = "https://files.pythonhosted.org/packages/55/b5/698bd0bf9fbb3ddb3a2feefbb7ad0dea1205f5d7d05b9cbab54f5db731aa/wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9", size = 83168 },
440 | { url = "https://files.pythonhosted.org/packages/ce/07/701a5cee28cb4d5df030d4b2649319e36f3d9fdd8000ef1d84eb06b9860d/wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339", size = 82307 },
441 | { url = "https://files.pythonhosted.org/packages/42/92/c48ba92cda6f74cb914dc3c5bba9650dc80b790e121c4b987f3a46b028f5/wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d", size = 75101 },
442 | { url = "https://files.pythonhosted.org/packages/8a/0a/9276d3269334138b88a2947efaaf6335f61d547698e50dff672ade24f2c6/wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b", size = 81835 },
443 | { url = "https://files.pythonhosted.org/packages/b9/4c/39595e692753ef656ea94b51382cc9aea662fef59d7910128f5906486f0e/wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346", size = 36412 },
444 | { url = "https://files.pythonhosted.org/packages/63/bb/c293a67fb765a2ada48f48cd0f2bb957da8161439da4c03ea123b9894c02/wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a", size = 38744 },
445 | { url = "https://files.pythonhosted.org/packages/85/82/518605474beafff11f1a34759f6410ab429abff9f7881858a447e0d20712/wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569", size = 38904 },
446 | { url = "https://files.pythonhosted.org/packages/80/6c/17c3b2fed28edfd96d8417c865ef0b4c955dc52c4e375d86f459f14340f1/wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504", size = 88622 },
447 | { url = "https://files.pythonhosted.org/packages/4a/11/60ecdf3b0fd3dca18978d89acb5d095a05f23299216e925fcd2717c81d93/wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451", size = 80920 },
448 | { url = "https://files.pythonhosted.org/packages/d2/50/dbef1a651578a3520d4534c1e434989e3620380c1ad97e309576b47f0ada/wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1", size = 89170 },
449 | { url = "https://files.pythonhosted.org/packages/44/a2/78c5956bf39955288c9e0dd62e807b308c3aa15a0f611fbff52aa8d6b5ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106", size = 86748 },
450 | { url = "https://files.pythonhosted.org/packages/99/49/2ee413c78fc0bdfebe5bee590bf3becdc1fab0096a7a9c3b5c9666b2415f/wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada", size = 79734 },
451 | { url = "https://files.pythonhosted.org/packages/c0/8c/4221b7b270e36be90f0930fe15a4755a6ea24093f90b510166e9ed7861ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4", size = 87552 },
452 | { url = "https://files.pythonhosted.org/packages/4c/6b/1aaccf3efe58eb95e10ce8e77c8909b7a6b0da93449a92c4e6d6d10b3a3d/wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635", size = 36647 },
453 | { url = "https://files.pythonhosted.org/packages/b3/4f/243f88ac49df005b9129194c6511b3642818b3e6271ddea47a15e2ee4934/wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7", size = 38830 },
454 | { url = "https://files.pythonhosted.org/packages/67/9c/38294e1bb92b055222d1b8b6591604ca4468b77b1250f59c15256437644f/wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181", size = 38904 },
455 | { url = "https://files.pythonhosted.org/packages/78/b6/76597fb362cbf8913a481d41b14b049a8813cd402a5d2f84e57957c813ae/wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393", size = 88608 },
456 | { url = "https://files.pythonhosted.org/packages/bc/69/b500884e45b3881926b5f69188dc542fb5880019d15c8a0df1ab1dfda1f7/wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4", size = 80879 },
457 | { url = "https://files.pythonhosted.org/packages/52/31/f4cc58afe29eab8a50ac5969963010c8b60987e719c478a5024bce39bc42/wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b", size = 89119 },
458 | { url = "https://files.pythonhosted.org/packages/aa/9c/05ab6bf75dbae7a9d34975fb6ee577e086c1c26cde3b6cf6051726d33c7c/wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721", size = 86778 },
459 | { url = "https://files.pythonhosted.org/packages/0e/6c/4b8d42e3db355603d35fe5c9db79c28f2472a6fd1ccf4dc25ae46739672a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90", size = 79793 },
460 | { url = "https://files.pythonhosted.org/packages/69/23/90e3a2ee210c0843b2c2a49b3b97ffcf9cad1387cb18cbeef9218631ed5a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a", size = 87606 },
461 | { url = "https://files.pythonhosted.org/packages/5f/06/3683126491ca787d8d71d8d340e775d40767c5efedb35039d987203393b7/wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045", size = 36651 },
462 | { url = "https://files.pythonhosted.org/packages/f1/bc/3bf6d2ca0d2c030d324ef9272bea0a8fdaff68f3d1fa7be7a61da88e51f7/wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838", size = 38835 },
463 | { url = "https://files.pythonhosted.org/packages/ce/b5/251165c232d87197a81cd362eeb5104d661a2dd3aa1f0b33e4bf61dda8b8/wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b", size = 40146 },
464 | { url = "https://files.pythonhosted.org/packages/89/33/1e1bdd3e866eeb73d8c4755db1ceb8a80d5bd51ee4648b3f2247adec4e67/wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379", size = 113444 },
465 | { url = "https://files.pythonhosted.org/packages/9f/7c/94f53b065a43f5dc1fbdd8b80fd8f41284315b543805c956619c0b8d92f0/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d", size = 101246 },
466 | { url = "https://files.pythonhosted.org/packages/62/5d/640360baac6ea6018ed5e34e6e80e33cfbae2aefde24f117587cd5efd4b7/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f", size = 109320 },
467 | { url = "https://files.pythonhosted.org/packages/e3/cf/6c7a00ae86a2e9482c91170aefe93f4ccda06c1ac86c4de637c69133da59/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c", size = 110193 },
468 | { url = "https://files.pythonhosted.org/packages/cd/cc/aa718df0d20287e8f953ce0e2f70c0af0fba1d3c367db7ee8bdc46ea7003/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b", size = 100460 },
469 | { url = "https://files.pythonhosted.org/packages/f7/16/9f3ac99fe1f6caaa789d67b4e3c562898b532c250769f5255fa8b8b93983/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", size = 106347 },
470 | { url = "https://files.pythonhosted.org/packages/64/85/c77a331b2c06af49a687f8b926fc2d111047a51e6f0b0a4baa01ff3a673a/wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", size = 37971 },
471 | { url = "https://files.pythonhosted.org/packages/05/9b/b2469f8be9efed24283fd7b9eeb8e913e9bc0715cf919ea8645e428ab7af/wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", size = 40755 },
472 | { url = "https://files.pythonhosted.org/packages/71/da/1c12502da116b379e511c39d95cdc8f886ace2f3478217cde9494d38ca58/wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13", size = 38712 },
473 | { url = "https://files.pythonhosted.org/packages/8a/b0/66f3e53c77257a505aaf7ef6d1b75ff7c8bb6a9da3d96f6aaa5810cd2f33/wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f", size = 86199 },
474 | { url = "https://files.pythonhosted.org/packages/08/4e/313f99f271557cc85b6ba086fb9a0d785d0373f237f30d0b4a4d14c7daed/wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c", size = 78073 },
475 | { url = "https://files.pythonhosted.org/packages/e1/62/5b50c324082081337c2b38daf4bae1de66e87eb126c754b0fa153b3525af/wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d", size = 85555 },
476 | { url = "https://files.pythonhosted.org/packages/eb/d2/31bb2c9362d84153d7597a471b22250783bf86be1a01c1acaba3bf7a0e01/wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce", size = 83892 },
477 | { url = "https://files.pythonhosted.org/packages/eb/54/f43889a2c787f2b8ac989461c0d2011f0ff69811ebf9b84796cc671aed63/wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627", size = 76869 },
478 | { url = "https://files.pythonhosted.org/packages/aa/37/0fbed8e67bd10b6f8835047abb6f42b8870689af45d7ae581946f1685468/wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f", size = 83564 },
479 | { url = "https://files.pythonhosted.org/packages/ef/3c/40db3a234871eda0a7eb48001d025474ed9fde85fd992eefda154ebc4632/wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea", size = 36407 },
480 | { url = "https://files.pythonhosted.org/packages/67/71/b9ce92b7820e9bd8e2c727d806a2e4e8c9d2a3e839ffadde2d0e44d84c0b/wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed", size = 38706 },
481 | { url = "https://files.pythonhosted.org/packages/89/03/518069f0708573c02cbba3a3e452be3642dc7d984d0a03a47e0850e2fb05/wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1", size = 38765 },
482 | { url = "https://files.pythonhosted.org/packages/60/01/12dd81522f8c1c953e98e2cbf356ff44fbb06ef0f7523cd622ac06ad7f03/wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c", size = 83012 },
483 | { url = "https://files.pythonhosted.org/packages/c4/2d/9853fe0009271b2841f839eb0e707c6b4307d169375f26c58812ecf4fd71/wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578", size = 74759 },
484 | { url = "https://files.pythonhosted.org/packages/94/5c/03c911442b01b50e364572581430e12f82c3f5ea74d302907c1449d7ba36/wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33", size = 82540 },
485 | { url = "https://files.pythonhosted.org/packages/52/e0/ef637448514295a6b3a01cf1dff417e081e7b8cf1eb712839962459af1f6/wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad", size = 81461 },
486 | { url = "https://files.pythonhosted.org/packages/7f/44/8b7d417c3aae3a35ccfe361375ee3e452901c91062e5462e1aeef98255e8/wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9", size = 74380 },
487 | { url = "https://files.pythonhosted.org/packages/af/a9/e65406a9c3a99162055efcb6bf5e0261924381228c0a7608066805da03df/wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0", size = 81057 },
488 | { url = "https://files.pythonhosted.org/packages/55/0c/111d42fb658a2f9ed7024cd5e57c08521d61646a256a3946db7d500c1551/wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88", size = 36415 },
489 | { url = "https://files.pythonhosted.org/packages/00/33/e7b14a7c06cedfaae064f34e95c95350de7cc10187ac173743e30a956b30/wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977", size = 38742 },
490 | { url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 },
491 | ]
492 |
--------------------------------------------------------------------------------