├── .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 | 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 | 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 | --------------------------------------------------------------------------------