├── .coveragerc
├── .editorconfig
├── .envrc
├── .envrc.use_venv
├── .github
└── workflows
│ ├── release-to-pypi.yml
│ ├── test-pypy27.yml
│ └── test.yml
├── .gitignore
├── .repos
├── CHANGES.txt
├── LICENSE
├── MANIFEST.in
├── README.rst
├── SECURITY.md
├── bin
├── github-workflow.json_schema
├── github-workflow_check.py
├── make_localpi.py
├── project_bootstrap.sh
└── toxcmd.py
├── invoke.yaml
├── justfile
├── parse_type
├── __init__.py
├── builder.py
├── cardinality.py
├── cardinality_field.py
├── cfparse.py
├── parse.py
└── parse_util.py
├── py.requirements
├── all.txt
├── basic.txt
├── ci.github.testing.txt
├── develop.txt
├── docs.txt
├── optional.txt
├── packaging.txt
└── testing.txt
├── pyproject.toml
├── pytest.ini
├── setup.cfg
├── setup.py
├── tasks
├── __init__.py
├── _compat_shutil.py
├── docs.py
├── invoke_dry_run.py
├── py.requirements.txt
├── release.py
└── test.py
├── tests
├── __init__.py
├── parse_tests
│ ├── __init__.py
│ ├── test_bugs.py
│ ├── test_findall.py
│ ├── test_parse.py
│ ├── test_parsetype.py
│ ├── test_pattern.py
│ ├── test_result.py
│ └── test_search.py
├── parse_tests_with_parse_type
│ ├── __init__.py
│ ├── test_bugs.py
│ ├── test_findall.py
│ ├── test_parse.py
│ ├── test_parsetype.py
│ ├── test_pattern.py
│ ├── test_result.py
│ └── test_search.py
├── parse_type_test.py
├── test_builder.py
├── test_cardinality.py
├── test_cardinality_field.py
├── test_cardinality_field0.py
├── test_cfparse.py
├── test_parse_decorator.py
├── test_parse_number.py
└── test_parse_util.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | # =========================================================================
2 | # COVERAGE CONFIGURATION FILE: .coveragerc
3 | # =========================================================================
4 | # LANGUAGE: Python
5 | # SEE ALSO:
6 | # * http://nedbatchelder.com/code/coverage/
7 | # * http://nedbatchelder.com/code/coverage/config.html
8 | # =========================================================================
9 |
10 | [run]
11 | # data_file = .coverage
12 | source = parse_type
13 | branch = True
14 | parallel = True
15 | omit = mock.py, ez_setup.py, distribute.py
16 |
17 |
18 | [report]
19 | ignore_errors = True
20 | show_missing = True
21 | # Regexes for lines to exclude from consideration
22 | exclude_lines =
23 | # Have to re-enable the standard pragma
24 | pragma: no cover
25 |
26 | # Don't complain about missing debug-only code:
27 | def __repr__
28 | if self\.debug
29 |
30 | # Don't complain if tests don't hit defensive assertion code:
31 | raise AssertionError
32 | raise NotImplementedError
33 |
34 | # Don't complain if non-runnable code isn't run:
35 | if 0:
36 | if False:
37 | if __name__ == .__main__.:
38 |
39 |
40 | [html]
41 | directory = build/coverage.html
42 | title = Coverage Report: parse_type
43 |
44 | [xml]
45 | output = build/coverage.xml
46 |
47 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # EDITOR CONFIGURATION: http://editorconfig.org
3 | # =============================================================================
4 |
5 | root = true
6 |
7 | # -- DEFAULT: Unix-style newlines with a newline ending every file.
8 | [*]
9 | charset = utf-8
10 | end_of_line = lf
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
14 | [*.{py,rst,ini,txt}]
15 | indent_style = space
16 | indent_size = 4
17 |
18 | [*.feature]
19 | indent_style = space
20 | indent_size = 2
21 |
22 | [**/makefile]
23 | indent_style = tab
24 |
25 | [*.{cmd,bat}]
26 | end_of_line = crlf
27 |
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 | # ===========================================================================
2 | # PROJECT ENVIRONMENT SETUP: parse_type/.envrc
3 | # ===========================================================================
4 | # SHELL: bash (or similiar)
5 | # USAGE:
6 | # # -- BETTER: Use direnv (requires: Setup in bash -- $HOME/.bashrc)
7 | # # BASH PROFILE NEEDS: eval "$(direnv hook bash)"
8 | # direnv allow .
9 | #
10 | # SIMPLISTIC ALTERNATIVE (without cleanup when directory scope is left again):
11 | # source .envrc
12 | #
13 | # SEE ALSO:
14 | # * https://direnv.net/
15 | # * https://peps.python.org/pep-0582/ Python local packages directory
16 | # ===========================================================================
17 | # MAYBE: HERE="${PWD}"
18 |
19 | # -- USE OPTIONAL PARTS (if exist/enabled):
20 | # DISABLED: dotenv_if_exists .env
21 | source_env_if_exists .envrc.use_venv
22 |
23 | # -- SETUP-PYTHON: Prepend ${HERE} to PYTHONPATH (as PRIMARY search path)
24 | # SIMILAR TO: export PYTHONPATH="${HERE}:${PYTHONPATH}"
25 | path_add PYTHONPATH .
26 | path_add PATH bin
27 |
28 | # DISABLED: source_env_if_exists .envrc.override
29 |
--------------------------------------------------------------------------------
/.envrc.use_venv:
--------------------------------------------------------------------------------
1 | # ===========================================================================
2 | # PROJECT ENVIRONMENT SETUP: parse_type/.envrc.use_venv
3 | # ===========================================================================
4 | # DESCRIPTION:
5 | # Setup and use a Python virtual environment (venv).
6 | # On entering the directory: Creates and activates a venv for a python version.
7 | # On leaving the directory: Deactivates the venv (virtual environment).
8 | #
9 | # SEE ALSO:
10 | # * https://direnv.net/
11 | # * https://github.com/direnv/direnv/wiki/Python
12 | # * https://direnv.net/man/direnv-stdlib.1.html#codelayout-python-ltpythonexegtcode
13 | # ===========================================================================
14 |
15 | # -- VIRTUAL ENVIRONMENT SUPPORT: layout python python3
16 | # VENV LOCATION: .direnv/python-$(PYTHON_VERSION)
17 | layout python python3
18 |
--------------------------------------------------------------------------------
/.github/workflows/release-to-pypi.yml:
--------------------------------------------------------------------------------
1 | # -- WORKFLOW: Publish/release this package on PyPI
2 | # SEE:
3 | # * https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
4 | # * https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-python#publishing-to-pypi
5 | #
6 | # * https://docs.github.com/en/actions/writing-workflows
7 | # * https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs
8 | # * https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#release
9 | #
10 | # GITHUB ACTIONS:
11 | # * https://github.com/actions/checkout
12 | # * https://github.com/pypa/gh-action-pypi-publish
13 | #
14 | # RELATED:
15 | # * https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml
16 |
17 | # -- STATE: PREPARED_ONLY, NOT_RELEASED_YET
18 | name: release-to-pypi
19 | on:
20 | release:
21 | types: [published]
22 | tags:
23 | - v0.*
24 | - v1.*
25 | permissions:
26 | contents: read
27 |
28 | jobs:
29 | publish-package:
30 | runs-on: ubuntu-latest
31 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
32 | environment:
33 | name: pypi
34 | url: https://pypi.org/p/parse-type
35 | permissions:
36 | id-token: write # REQUIRED-FOR: Trusted publishing.
37 | steps:
38 | - uses: actions/checkout@v4
39 | - uses: actions/setup-python@v5
40 | with:
41 | python-version: "3.10"
42 | - name: "Install Python package dependencies (with: uv)"
43 | run: |
44 | python -m pip install -U uv
45 | python -m uv pip install -U pip setuptools wheel build twine
46 | - name: Build this package
47 | run: python -m build
48 | - name: Check this package (before upload)
49 | run: twine check dist/*
50 | - name: Upload this package to PyPI
51 | uses: pypa/gh-action-pypi-publish@release/v1
52 | with:
53 | print-hash: true
54 | verbose: true
55 |
--------------------------------------------------------------------------------
/.github/workflows/test-pypy27.yml:
--------------------------------------------------------------------------------
1 | # -- TEST-VARIANT: pypy-27 on ubuntu-latest
2 | # BASED ON: test.yml
3 | # DESCRIPTION: Checks for Python 2.7 support and any problems
4 |
5 | name: test-pypy27
6 | on:
7 | workflow_dispatch:
8 | push:
9 | branches: [ "main", "release/**" ]
10 | pull_request:
11 | types: [opened, reopened, review_requested]
12 | branches: [ "main" ]
13 |
14 | jobs:
15 | test:
16 | runs-on: ubuntu-latest
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | python-version: ["pypy-2.7"]
21 | steps:
22 | - uses: actions/checkout@v4
23 | - uses: actions/setup-python@v5
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 | cache: 'pip'
27 | cache-dependency-path: 'py.requirements/*.txt'
28 |
29 | - name: Install Python package dependencies
30 | run: |
31 | python -m pip install -U pip setuptools wheel
32 | pip install --upgrade -r py.requirements/ci.github.testing.txt
33 | pip install -e .
34 | - name: Run tests
35 | run: pytest
36 | - name: Upload test reports
37 | uses: actions/upload-artifact@v4
38 | with:
39 | name: test reports
40 | path: |
41 | build/testing/report.xml
42 | build/testing/report.html
43 | if: ${{ job.status == 'failure' }}
44 | # MAYBE: if: ${{ always() }}
45 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # -- SOURCE: https://github.com/marketplace/actions/setup-python
2 | # SEE: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 | # SUPPORTED PYTHON VERSIONS: https://github.com/actions/python-versions
4 |
5 | name: test
6 | on:
7 | workflow_dispatch:
8 | push:
9 | branches: [ "main", "release/**" ]
10 | pull_request:
11 | types: [opened, reopened, review_requested]
12 | branches: [ "main" ]
13 |
14 | jobs:
15 | test:
16 | # -- EXAMPLE: runs-on: ubuntu-latest
17 | runs-on: ${{ matrix.os }}
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | # PREPARED: os: [ubuntu-latest, macos-latest, windows-latest]
22 | os: [ubuntu-latest, windows-latest]
23 | python-version: ["3.13.0-rc.3", "3.12", "3.11", "3.10"]
24 | exclude:
25 | - os: windows-latest
26 | python-version: "2.7"
27 | steps:
28 | - uses: actions/checkout@v4
29 | # DISABLED: name: Setup Python ${{ matrix.python-version }} on platform=${{ matrix.os }}
30 | - uses: actions/setup-python@v5
31 | with:
32 | python-version: ${{ matrix.python-version }}
33 | cache: 'pip'
34 | cache-dependency-path: 'py.requirements/*.txt'
35 | - name: setup-uv -- Speed-up Python package installations ...
36 | uses: astral-sh/setup-uv@v3
37 | with:
38 | enable-cache: true
39 | cache-dependency-glob: |
40 | **/pyproject.toml
41 | **/py.requirements/ci.github.testing.txt
42 | **/py.requirements/basic.txt
43 | - name: "Install Python package dependencies (with: uv)"
44 | run: |
45 | uv pip install --system -U pip setuptools wheel
46 | uv pip install --system -U -r py.requirements/ci.github.testing.txt
47 | uv pip install --system -e .
48 | - name: Run tests
49 | run: pytest
50 | - name: Upload test reports
51 | uses: actions/upload-artifact@v3
52 | with:
53 | name: test reports
54 | path: |
55 | build/testing/report.xml
56 | build/testing/report.html
57 | if: ${{ job.status == 'failure' }}
58 | # MAYBE: if: ${{ always() }}
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # -- TEMPORARY PYTHON PACKAGE PARTS:
4 | parse_type/_version.py
5 | MANIFEST
6 | *.egg
7 | *.egg-info
8 | dist
9 | build
10 | downloads
11 | __pycache__
12 |
13 | # Installer logs
14 | pip-log.txt
15 |
16 | Pipfile
17 | Pipfile.lock
18 |
19 | # -- TESTS, COVERAGE REPORTS, ...
20 | .cache/
21 | .direnv/
22 | .eggs/
23 | .pytest_cache/
24 | .ruff_cache/
25 | .tox/
26 | .venv*/
27 | .coverage
28 | .done.*
29 |
30 | # -- IDE-RELATED:
31 | .fleet/
32 | .idea/
33 | .vscode/
34 | .project
35 | .pydevproject
36 |
37 | # -- EXCLUDE GIT-SUBPROJECTS:
38 | /lib/parse/
39 |
--------------------------------------------------------------------------------
/.repos:
--------------------------------------------------------------------------------
1 | # ===========================================================================
2 | # vcs: Multi-repo configuration
3 | # ===========================================================================
4 | # USAGE:
5 | # vcs --commands # Show available commands
6 | #
7 | # vcs import < .repos
8 | # vcs import --input=.repos
9 | # vcs import --input=https://github.com/jenisys/cxx.simplelog/blob/master/.repos
10 | # vcs import --input=https://github.com/jenisys/cxx.simplelog/blob/master/.rosinstall
11 | # vcs import --shallow --input=.repos
12 | # vcs import lib/ --input=.repos
13 | #
14 | # vcs pull
15 | # vcs status
16 | #
17 | # vcs export --nested # Use branch-name
18 | # vcs export --nested --exact # Use commit-hashes instead of branch-name
19 | # vcs export --nested --exact-with-tags # Use tags or commit-hashes
20 | # vcs export --nested lib/doctest # For a specific path instead of ".".
21 | #
22 | # BAD: vcs-export adds basename of current-directory to repositories.
23 | #
24 | # SEE ALSO:
25 | # * https://github.com/dirk-thomas/vcstool
26 | # ===========================================================================
27 | # REQUIRES: pip install vcstool
28 |
29 | repositories:
30 | lib/parse:
31 | type: git
32 | url: https://github.com/r1chardj0n3s/parse.git
33 | version: master
34 |
--------------------------------------------------------------------------------
/CHANGES.txt:
--------------------------------------------------------------------------------
1 | Version History
2 | ===============================================================================
3 |
4 |
5 | Version: 0.7.0 (UNRELEASED)
6 | -------------------------------------------------------------------------------
7 |
8 | GOALS:
9 |
10 | * Drop support for Python 2.7
11 | * Support Python >= 3.7 (probably)
12 |
13 | Version: 0.6.2 (2023-07-04)
14 | -------------------------------------------------------------------------------
15 |
16 | FIXES:
17 |
18 | * #21: tests/test_parse.py tests ``parse_type.parse`` (per default).
19 | REASON: Using for older installed ``parse`` module may cause weird problems.
20 | RELATED TO: ``parse v1.19.1`` (behavior changed compared to ``v1.19.0``)
21 |
22 |
23 | Version: 0.6.1 (2023-07-02)
24 | -------------------------------------------------------------------------------
25 |
26 | * Switch to MIT license (same as: `parse`_ module)
27 | * Use SPDX-License-Identifier in source code (to simplify understanding)
28 | * UPDATE/SYNC to `parse`_ v1.19.1
29 | * ADDED: ``pyproject.toml`` to support newer ``pip`` versions
30 | REASON: ``setup.py`` becomes DEPRECATED in 2023-09 for newer ``pip`` versions.
31 |
32 | FIXED:
33 |
34 | * Issue #19: 0.6.0: pytest is failing in two units (submitted by: kloczek; caused by: `parse`_ v1.19.1)
35 | * Issue #1: Licensing confusion
36 |
37 | DEVELOPMENT:
38 |
39 | * VCS: Renamed default branch of Git repository to "main" (was: "master").
40 | * CI: Use github-actions as CI pipeline.
41 |
42 |
43 | Version: 0.6.0 (2022-01-18)
44 | -------------------------------------------------------------------------------
45 |
46 | FIXED:
47 |
48 | + issue #17: setup.py: Remove use of "use_2to3" (submitted by: xxx)
49 |
50 |
51 | Version: 0.5.6 (2020-09-11)
52 | -------------------------------------------------------------------------------
53 |
54 | FIXED:
55 |
56 | + parse issue #119 (same as: #121): int_convert memory effect with number-base discovery
57 | + UPDATE to parse v1.18.0 (needed by: parse issue #119)
58 |
59 |
60 | Version: 0.5.5 (2020-09-10)
61 | -------------------------------------------------------------------------------
62 |
63 | FIXED:
64 |
65 | + parse PR #122: Fixes issue #121 in parse: int_convert memory effect.
66 |
67 |
68 | Version: 0.5.4 (2020-09-10)
69 | -------------------------------------------------------------------------------
70 |
71 | UPDATED:
72 |
73 | + parse v1.17.0
74 |
75 |
76 | Version: 0.5.3 (2019-12-15)
77 | -------------------------------------------------------------------------------
78 |
79 | UPDATED:
80 |
81 | + setup.py: Add support for Python 3.8.
82 | + UPDATE: Dependencies
83 |
84 |
85 | Version: 0.5.2 (2019-07-14)
86 | -------------------------------------------------------------------------------
87 |
88 | UPDATED:
89 |
90 | + parse v1.12.0
91 |
92 | FIXED:
93 |
94 | + Python3 DeprecationWarning for regex (here: in docstrings).
95 |
96 |
97 | Version: 0.5.1 (2018-05-27)
98 | -------------------------------------------------------------------------------
99 |
100 | CHANGED:
101 |
102 | + Add parse_type.cfparse.Parser(..., case_sensitive=False, ...) parameter
103 | to match functionality in parse.Parser constructor (in parse-1.8.4).
104 | + UPDATE to parse-1.8.4
105 |
106 |
107 | Version: 0.5.0 (2018-04-08; includes: v0.4.3)
108 | -------------------------------------------------------------------------------
109 |
110 | FIXED:
111 |
112 | + FIX doctest for parse_type.parse module.
113 |
114 | CHANGES:
115 |
116 | * UPDATE: parse-1.8.3 (was: parse-1.8.2)
117 | NOTE: ``parse`` module and ``parse_type.parse`` module are now identical.
118 |
119 | BACKWARD INCOMPATIBLE CHANGES:
120 |
121 | * RENAMED: type_converter.regex_group_count attribute (was: .group_count)
122 | (pull-request review changes of the ``parse`` module).
123 |
124 |
125 | .. _parse: https://github.com/r1chardj0n3s/parse
126 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2013-2023 jenisys
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | include LICENSE
3 | include .coveragerc
4 | include .editorconfig
5 | include *.py
6 | include *.rst
7 | include *.txt
8 | include *.ini
9 | include *.cfg
10 | include *.yaml
11 | include bin/invoke*
12 | exclude __*.rst
13 | exclude __*.txt
14 |
15 | recursive-include bin *.cmd *.py *.sh
16 | recursive-include py.requirements *.txt
17 | recursive-include tasks *.py *.txt
18 | recursive-include tests *.py
19 | # -- DISABLED: recursive-include docs *.rst *.txt *.py
20 |
21 | prune .direnv
22 | prune .tox
23 | prune .venv*
24 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ===============================================================================
2 | parse_type
3 | ===============================================================================
4 |
5 | .. |badge.CI_status| image:: https://github.com/jenisys/parse_type/actions/workflows/test.yml/badge.svg
6 | :target: https://github.com/jenisys/parse_type/actions/workflows/test.yml
7 | :alt: CI Build Status
8 |
9 | .. |badge.latest_version| image:: https://img.shields.io/pypi/v/parse_type.svg
10 | :target: https://pypi.python.org/pypi/parse_type
11 | :alt: Latest Version
12 |
13 | .. |badge.downloads| image:: https://img.shields.io/pypi/dm/parse_type.svg
14 | :target: https://pypi.python.org/pypi/parse_type
15 | :alt: Downloads
16 |
17 | .. |badge.license| image:: https://img.shields.io/pypi/l/parse_type.svg
18 | :target: https://pypi.python.org/pypi/parse_type/
19 | :alt: License
20 |
21 | |badge.CI_status| |badge.latest_version| |badge.license| |badge.downloads|
22 |
23 | `parse_type`_ extends the `parse`_ module (opposite of `string.format()`_)
24 | with the following features:
25 |
26 | * build type converters for common use cases (enum/mapping, choice)
27 | * build a type converter with a cardinality constraint (0..1, 0..*, 1..*)
28 | from the type converter with cardinality=1.
29 | * compose a type converter from other type converters
30 | * an extended parser that supports the CardinalityField naming schema
31 | and creates missing type variants (0..1, 0..*, 1..*) from the
32 | primary type converter
33 |
34 | .. _parse_type: http://pypi.python.org/pypi/parse_type
35 | .. _parse: http://pypi.python.org/pypi/parse
36 | .. _`string.format()`: http://docs.python.org/library/string.html#format-string-syntax
37 |
38 |
39 | Definitions
40 | -------------------------------------------------------------------------------
41 |
42 | *type converter*
43 | A type converter function that converts a textual representation
44 | of a value type into instance of this value type.
45 | In addition, a type converter function is often annotated with attributes
46 | that allows the `parse`_ module to use it in a generic way.
47 | A type converter is also called a *parse_type* (a definition used here).
48 |
49 | *cardinality field*
50 | A naming convention for related types that differ in cardinality.
51 | A cardinality field is a type name suffix in the format of a field.
52 | It allows parse format expression, ala::
53 |
54 | "{person:Person}" #< Cardinality: 1 (one; the normal case)
55 | "{person:Person?}" #< Cardinality: 0..1 (zero or one = optional)
56 | "{persons:Person*}" #< Cardinality: 0..* (zero or more = many0)
57 | "{persons:Person+}" #< Cardinality: 1..* (one or more = many)
58 |
59 | This naming convention mimics the relationship descriptions in UML diagrams.
60 |
61 |
62 | Basic Example
63 | -------------------------------------------------------------------------------
64 |
65 | Define an own type converter for numbers (integers):
66 |
67 | .. code-block:: python
68 |
69 | # -- USE CASE:
70 | def parse_number(text):
71 | return int(text)
72 | parse_number.pattern = r"\d+" # -- REGULAR EXPRESSION pattern for type.
73 |
74 | This is equivalent to:
75 |
76 | .. code-block:: python
77 |
78 | import parse
79 |
80 | @parse.with_pattern(r"\d+")
81 | def parse_number(text):
82 | return int(text)
83 | assert hasattr(parse_number, "pattern")
84 | assert parse_number.pattern == r"\d+"
85 |
86 |
87 | .. code-block:: python
88 |
89 | # -- USE CASE: Use the type converter with the parse module.
90 | schema = "Hello {number:Number}"
91 | parser = parse.Parser(schema, dict(Number=parse_number))
92 | result = parser.parse("Hello 42")
93 | assert result is not None, "REQUIRE: text matches the schema."
94 | assert result["number"] == 42
95 |
96 | result = parser.parse("Hello XXX")
97 | assert result is None, "MISMATCH: text does not match the schema."
98 |
99 | .. hint::
100 |
101 | The described functionality above is standard functionality
102 | of the `parse`_ module. It serves as introduction for the remaining cases.
103 |
104 |
105 | Cardinality
106 | -------------------------------------------------------------------------------
107 |
108 | Create an type converter for "ManyNumbers" (List, separated with commas)
109 | with cardinality "1..* = 1+" (many) from the type converter for a "Number".
110 |
111 | .. code-block:: python
112 |
113 | # -- USE CASE: Create new type converter with a cardinality constraint.
114 | # CARDINALITY: many := one or more (1..*)
115 | from parse import Parser
116 | from parse_type import TypeBuilder
117 | parse_numbers = TypeBuilder.with_many(parse_number, listsep=",")
118 |
119 | schema = "List: {numbers:ManyNumbers}"
120 | parser = Parser(schema, dict(ManyNumbers=parse_numbers))
121 | result = parser.parse("List: 1, 2, 3")
122 | assert result["numbers"] == [1, 2, 3]
123 |
124 |
125 | Create an type converter for an "OptionalNumbers" with cardinality "0..1 = ?"
126 | (optional) from the type converter for a "Number".
127 |
128 | .. code-block:: python
129 |
130 | # -- USE CASE: Create new type converter with cardinality constraint.
131 | # CARDINALITY: optional := zero or one (0..1)
132 | from parse import Parser
133 | from parse_type import TypeBuilder
134 |
135 | parse_optional_number = TypeBuilder.with_optional(parse_number)
136 | schema = "Optional: {number:OptionalNumber}"
137 | parser = Parser(schema, dict(OptionalNumber=parse_optional_number))
138 | result = parser.parse("Optional: 42")
139 | assert result["number"] == 42
140 | result = parser.parse("Optional: ")
141 | assert result["number"] == None
142 |
143 |
144 | Enumeration (Name-to-Value Mapping)
145 | -------------------------------------------------------------------------------
146 |
147 | Create an type converter for an "Enumeration" from the description of
148 | the mapping as dictionary.
149 |
150 | .. code-block:: python
151 |
152 | # -- USE CASE: Create a type converter for an enumeration.
153 | from parse import Parser
154 | from parse_type import TypeBuilder
155 |
156 | parse_enum_yesno = TypeBuilder.make_enum({"yes": True, "no": False})
157 | parser = Parser("Answer: {answer:YesNo}", dict(YesNo=parse_enum_yesno))
158 | result = parser.parse("Answer: yes")
159 | assert result["answer"] == True
160 |
161 |
162 | Create an type converter for an "Enumeration" from the description of
163 | the mapping as an enumeration class (`Python 3.4 enum`_ or the `enum34`_
164 | backport; see also: `PEP-0435`_).
165 |
166 | .. code-block:: python
167 |
168 | # -- USE CASE: Create a type converter for enum34 enumeration class.
169 | # NOTE: Use Python 3.4 or enum34 backport.
170 | from parse import Parser
171 | from parse_type import TypeBuilder
172 | from enum import Enum
173 |
174 | class Color(Enum):
175 | red = 1
176 | green = 2
177 | blue = 3
178 |
179 | parse_enum_color = TypeBuilder.make_enum(Color)
180 | parser = Parser("Select: {color:Color}", dict(Color=parse_enum_color))
181 | result = parser.parse("Select: red")
182 | assert result["color"] is Color.red
183 |
184 | .. _`Python 3.4 enum`: http://docs.python.org/3.4/library/enum.html#module-enum
185 | .. _enum34: http://pypi.python.org/pypi/enum34
186 | .. _PEP-0435: http://www.python.org/dev/peps/pep-0435
187 |
188 |
189 | Choice (Name Enumeration)
190 | -------------------------------------------------------------------------------
191 |
192 | A Choice data type allows to select one of several strings.
193 |
194 | Create an type converter for an "Choice" list, a list of unique names
195 | (as string).
196 |
197 | .. code-block:: python
198 |
199 | from parse import Parser
200 | from parse_type import TypeBuilder
201 |
202 | parse_choice_yesno = TypeBuilder.make_choice(["yes", "no"])
203 | schema = "Answer: {answer:ChoiceYesNo}"
204 | parser = Parser(schema, dict(ChoiceYesNo=parse_choice_yesno))
205 | result = parser.parse("Answer: yes")
206 | assert result["answer"] == "yes"
207 |
208 |
209 | Variant (Type Alternatives)
210 | -------------------------------------------------------------------------------
211 |
212 | Sometimes you need a type converter that can accept text for multiple
213 | type converter alternatives. This is normally called a "variant" (or: union).
214 |
215 | Create an type converter for an "Variant" type that accepts:
216 |
217 | * Numbers (positive numbers, as integer)
218 | * Color enum values (by name)
219 |
220 | .. code-block:: python
221 |
222 | from parse import Parser, with_pattern
223 | from parse_type import TypeBuilder
224 | from enum import Enum
225 |
226 | class Color(Enum):
227 | red = 1
228 | green = 2
229 | blue = 3
230 |
231 | @with_pattern(r"\d+")
232 | def parse_number(text):
233 | return int(text)
234 |
235 | # -- MAKE VARIANT: Alternatives of different type converters.
236 | parse_color = TypeBuilder.make_enum(Color)
237 | parse_variant = TypeBuilder.make_variant([parse_number, parse_color])
238 | schema = "Variant: {variant:Number_or_Color}"
239 | parser = Parser(schema, dict(Number_or_Color=parse_variant))
240 |
241 | # -- TEST VARIANT: With number, color and mismatch.
242 | result = parser.parse("Variant: 42")
243 | assert result["variant"] == 42
244 | result = parser.parse("Variant: blue")
245 | assert result["variant"] is Color.blue
246 | result = parser.parse("Variant: __MISMATCH__")
247 | assert not result
248 |
249 |
250 |
251 | Extended Parser with CardinalityField support
252 | -------------------------------------------------------------------------------
253 |
254 | The parser extends the ``parse.Parser`` and adds the following functionality:
255 |
256 | * supports the CardinalityField naming scheme
257 | * automatically creates missing type variants for types with
258 | a CardinalityField by using the primary type converter for cardinality=1
259 | * extends the provide type converter dictionary with new type variants.
260 |
261 | Example:
262 |
263 | .. code-block:: python
264 |
265 | # -- USE CASE: Parser with CardinalityField support.
266 | # NOTE: Automatically adds missing type variants with CardinalityField part.
267 | # USE: parse_number() type converter from above.
268 | from parse_type.cfparse import Parser
269 |
270 | # -- PREPARE: parser, adds missing type variant for cardinality 1..* (many)
271 | type_dict = dict(Number=parse_number)
272 | schema = "List: {numbers:Number+}"
273 | parser = Parser(schema, type_dict)
274 | assert "Number+" in type_dict, "Created missing type variant based on: Number"
275 |
276 | # -- USE: parser.
277 | result = parser.parse("List: 1, 2, 3")
278 | assert result["numbers"] == [1, 2, 3]
279 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | The following versions are currently being supported with security updates.
6 |
7 | | Version | Supported |
8 | | ------- | ------------------ |
9 | | HEAD | :white_check_mark: |
10 | | 0.6.x | :white_check_mark: |
11 | | < 0.6.0 | :x: |
12 |
13 | ## Reporting a Vulnerability
14 |
15 | Please report security issues by using the new
16 | [Github vulnerability reporting mechanism][security advisories for this repository]
17 | that is enabled for [this repository].
18 |
19 | SEE ALSO:
20 |
21 | * [THIS REPOSITORY]: [Security Advisories][security advisories for this repository]
22 | * [docs.github.com]/.../[guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability]
23 |
24 |
25 | [this repository]: https://github.com/jenisys/parse_type
26 | [security advisories for this repository]: https://github.com/jenisys/parse_type/security/advisories
27 |
28 | [docs.github.com]: https://docs.github.com/en/
29 | [guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability
30 |
--------------------------------------------------------------------------------
/bin/github-workflow_check.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Check a github-workflow YAML file.
4 |
5 | RELATED: JSON schema for Github Action workflows
6 |
7 | * https://dev.to/robertobutti/vscode-how-to-check-workflow-syntax-for-github-actions-4k0o
8 |
9 | - https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions
10 | - https://github.com/actions/starter-workflows/tree/master/ci
11 | - https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml
12 |
13 | REQUIRES:
14 | pip install check-jsonschema
15 | DOWNLOAD: https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/github-workflow.json
16 | USE: check-jsonschema --schemafile github-workflow.json_schema.txt .github/workflows/release-to-pypi.yml
17 |
18 | * https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/github-action.json
19 | * MAYBE: https://github.com/softprops/github-actions-schemas/blob/master/workflow.json
20 |
21 | REQUIRES:
22 |
23 | * pip install check-jsonschema
24 | * pip install typer >= 0.12.5
25 | * pip install typing-extensions
26 |
27 | GITHUB WORKFLOW SCHEMA:
28 |
29 | * https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/github-workflow.json
30 | """
31 |
32 | from pathlib import Path
33 | from subprocess import run
34 | from typing import Optional
35 | from typing_extensions import Self
36 |
37 | import typer
38 |
39 | # -----------------------------------------------------------------------------
40 | # CONSTANTS
41 | # -----------------------------------------------------------------------------
42 | HERE = Path(__file__).parent.absolute()
43 | GITHUB_WORKFLOW_SCHEMA_URL = "https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/github-workflow.json"
44 | GITHUB_WORKFLOW_SCHEMA_PATH = HERE/"github-workflow.json_schema"
45 |
46 |
47 | # -----------------------------------------------------------------------------
48 | # CLASSES:
49 | # -----------------------------------------------------------------------------
50 | class Verdict:
51 | def __init__(self, path: Path, outcome: bool, message: Optional[str] = None):
52 | self.path = path
53 | self.outcome = outcome
54 | self.message = message or ""
55 |
56 | @property
57 | def verdict(self):
58 | the_verdict = "FAILED"
59 | if self.outcome:
60 | the_verdict = "OK"
61 | return the_verdict
62 |
63 | def as_bool(self):
64 | return bool(self.outcome)
65 |
66 | def __bool__(self):
67 | return self.as_bool()
68 |
69 | def __str__(self):
70 | return f"{self.verdict}: {self.path} {self.message}".strip()
71 |
72 | def __repr__(self):
73 | class_name = self.__class__.__name__
74 | return f"<{class_name}: path={self.path}, verdict={self.verdict}, message='{self.message}'>"
75 |
76 | @classmethod
77 | def make_success(cls, path: Path, message: Optional[str] = None) -> Self:
78 | return cls(path, outcome=True, message=message)
79 |
80 | @classmethod
81 | def make_failure(cls, path: Path, message: Optional[str] = None) -> Self:
82 | return cls(path, outcome=False, message=message)
83 |
84 |
85 | def workflow_check(path: Path) -> Verdict:
86 | schema = GITHUB_WORKFLOW_SCHEMA_PATH
87 |
88 | print(f"CHECK: {path} ... ")
89 | result = run(["check-jsonschema", f"--schemafile={schema}", f"{path}"])
90 | if result.returncode == 0:
91 | return Verdict.make_success(path)
92 | # -- OTHERWISE:
93 | return Verdict.make_failure(path)
94 |
95 |
96 | def workflow_check_many(paths: list[Path]) -> list[Verdict]:
97 | verdicts = []
98 | for path in paths:
99 | verdict = workflow_check(path)
100 | verdicts.append(verdict)
101 | return verdicts
102 |
103 |
104 | def main(paths: list[Path]) -> int:
105 | """
106 | Check github-workflow YAML file(s).
107 |
108 | :param paths: Paths to YAML file(s).
109 | :return: 0, if all checks pass. 1, otherwise
110 | """
111 | verdicts = workflow_check_many(paths)
112 |
113 | count_passed = 0
114 | count_failed = 0
115 | for verdict in verdicts:
116 | # DISABLED: print(str(verdict))
117 | if verdict:
118 | count_passed += 1
119 | else:
120 | count_failed += 1
121 |
122 | summary = f"SUMMARY: {len(verdicts)} files, {count_passed} passed, {count_failed} failed"
123 | print(summary)
124 | result = 1
125 | if count_failed == 0:
126 | result = 0
127 | return result
128 |
129 |
130 | if __name__ == '__main__':
131 | typer.run(main)
132 |
--------------------------------------------------------------------------------
/bin/make_localpi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | Utility script to create a pypi-like directory structure (localpi)
5 | from a number of Python packages in a directory of the local filesystem.
6 |
7 | DIRECTORY STRUCTURE (before):
8 | +-- downloads/
9 | +-- alice-1.0.zip
10 | +-- alice-1.0.tar.gz
11 | +-- bob-1.3.0.tar.gz
12 | +-- bob-1.4.2.tar.gz
13 | +-- charly-1.0.tar.bz2
14 |
15 | DIRECTORY STRUCTURE (afterwards):
16 | +-- downloads/
17 | +-- simple/
18 | | +-- alice/index.html --> ../../alice-*.*
19 | | +-- bob/index.html --> ../../bob-*.*
20 | | +-- charly/index.html --> ../../charly-*.*
21 | | +-- index.html --> alice/, bob/, ...
22 | +-- alice-1.0.zip
23 | +-- alice-1.0.tar.gz
24 | +-- bob-1.3.0.tar.gz
25 | +-- bob-1.4.2.tar.gz
26 | +-- charly-1.0.tar.bz2
27 |
28 | USAGE EXAMPLE:
29 |
30 | mkdir -p /tmp/downloads
31 | pip install --download=/tmp/downloads argparse Jinja2
32 | make_localpi.py /tmp/downloads
33 | pip install --index-url=file:///tmp/downloads/simple argparse Jinja2
34 |
35 | ALTERNATIVE:
36 |
37 | pip install --download=/tmp/downloads argparse Jinja2
38 | pip install --find-links=/tmp/downloads --no-index argparse Jinja2
39 | """
40 |
41 | from __future__ import with_statement, print_function
42 | from fnmatch import fnmatch
43 | import os.path
44 | import shutil
45 | import sys
46 |
47 |
48 | __author__ = "Jens Engel"
49 | __version__ = "0.2"
50 | __license__ = "BSD"
51 | __copyright__ = "(c) 2013 by Jens Engel"
52 |
53 |
54 | class Package(object):
55 | """
56 | Package entity that keeps track of:
57 | * one or more versions of this package
58 | * one or more archive types
59 | """
60 | PATTERNS = [
61 | "*.egg", "*.exe", "*.whl", "*.zip", "*.tar.gz", "*.tar.bz2", "*.7z"
62 | ]
63 |
64 | def __init__(self, filename, name=None):
65 | if not name and filename:
66 | name = self.get_pkgname(filename)
67 | self.name = name
68 | self.files = []
69 | if filename:
70 | self.files.append(filename)
71 |
72 | @property
73 | def versions(self):
74 | versions_info = [ self.get_pkgversion(p) for p in self.files ]
75 | return versions_info
76 |
77 | @classmethod
78 | def get_pkgversion(cls, filename):
79 | parts = os.path.basename(filename).rsplit("-", 1)
80 | version = ""
81 | if len(parts) >= 2:
82 | version = parts[1]
83 | for pattern in cls.PATTERNS:
84 | assert pattern.startswith("*")
85 | suffix = pattern[1:]
86 | if version.endswith(suffix):
87 | version = version[:-len(suffix)]
88 | break
89 | return version
90 |
91 | @staticmethod
92 | def get_pkgname(filename):
93 | name = os.path.basename(filename).rsplit("-", 1)[0]
94 | if name.startswith("http%3A") or name.startswith("https%3A"):
95 | # -- PIP DOWNLOAD-CACHE PACKAGE FILE NAME SCHEMA:
96 | pos = name.rfind("%2F")
97 | name = name[pos+3:]
98 | return name
99 |
100 | @staticmethod
101 | def splitext(filename):
102 | fname = os.path.splitext(filename)[0]
103 | if fname.endswith(".tar"):
104 | fname = os.path.splitext(fname)[0]
105 | return fname
106 |
107 | @classmethod
108 | def isa(cls, filename):
109 | basename = os.path.basename(filename)
110 | if basename.startswith("."):
111 | return False
112 | for pattern in cls.PATTERNS:
113 | if fnmatch(filename, pattern):
114 | return True
115 | return False
116 |
117 |
118 | def make_index_for(package, index_dir, verbose=True):
119 | """
120 | Create an 'index.html' for one package.
121 |
122 | :param package: Package object to use.
123 | :param index_dir: Where 'index.html' should be created.
124 | """
125 | index_template = """\
126 |
127 |
{title}
128 |
129 | {title}
130 |
133 |
134 |
135 | """
136 | item_template = '{0}'
137 | index_filename = os.path.join(index_dir, "index.html")
138 | if not os.path.isdir(index_dir):
139 | os.makedirs(index_dir)
140 |
141 | parts = []
142 | for pkg_filename in package.files:
143 | pkg_name = os.path.basename(pkg_filename)
144 | if pkg_name == "index.html":
145 | # -- ROOT-INDEX:
146 | pkg_name = os.path.basename(os.path.dirname(pkg_filename))
147 | else:
148 | pkg_name = package.splitext(pkg_name)
149 | pkg_relpath_to = os.path.relpath(pkg_filename, index_dir)
150 | parts.append(item_template.format(pkg_name, pkg_relpath_to))
151 |
152 | if not parts:
153 | print("OOPS: Package %s has no files" % package.name)
154 | return
155 |
156 | if verbose:
157 | root_index = not Package.isa(package.files[0])
158 | if root_index:
159 | info = "with %d package(s)" % len(package.files)
160 | else:
161 | package_versions = sorted(set(package.versions))
162 | info = ", ".join(reversed(package_versions))
163 | message = "%-30s %s" % (package.name, info)
164 | print(message)
165 |
166 | with open(index_filename, "w") as f:
167 | packages = "\n".join(parts)
168 | text = index_template.format(title=package.name, packages=packages)
169 | f.write(text.strip())
170 | f.close()
171 |
172 |
173 | def make_package_index(download_dir):
174 | """
175 | Create a pypi server like file structure below download directory.
176 |
177 | :param download_dir: Download directory with packages.
178 |
179 | EXAMPLE BEFORE:
180 | +-- downloads/
181 | +-- alice-1.0.zip
182 | +-- alice-1.0.tar.gz
183 | +-- bob-1.3.0.tar.gz
184 | +-- bob-1.4.2.tar.gz
185 | +-- charly-1.0.tar.bz2
186 |
187 | EXAMPLE AFTERWARDS:
188 | +-- downloads/
189 | +-- simple/
190 | | +-- alice/index.html --> ../../alice-*.*
191 | | +-- bob/index.html --> ../../bob-*.*
192 | | +-- charly/index.html --> ../../charly-*.*
193 | | +-- index.html --> alice/index.html, bob/index.html, ...
194 | +-- alice-1.0.zip
195 | +-- alice-1.0.tar.gz
196 | +-- bob-1.3.0.tar.gz
197 | +-- bob-1.4.2.tar.gz
198 | +-- charly-1.0.tar.bz2
199 | """
200 | if not os.path.isdir(download_dir):
201 | raise ValueError("No such directory: %r" % download_dir)
202 |
203 | pkg_rootdir = os.path.join(download_dir, "simple")
204 | if os.path.isdir(pkg_rootdir):
205 | shutil.rmtree(pkg_rootdir, ignore_errors=True)
206 | os.mkdir(pkg_rootdir)
207 |
208 | # -- STEP: Collect all packages.
209 | package_map = {}
210 | packages = []
211 | for filename in sorted(os.listdir(download_dir)):
212 | if not Package.isa(filename):
213 | continue
214 | pkg_filepath = os.path.join(download_dir, filename)
215 | package_name = Package.get_pkgname(pkg_filepath)
216 | package = package_map.get(package_name, None)
217 | if not package:
218 | # -- NEW PACKAGE DETECTED: Store/register package.
219 | package = Package(pkg_filepath)
220 | package_map[package.name] = package
221 | packages.append(package)
222 | else:
223 | # -- SAME PACKAGE: Collect other variant/version.
224 | package.files.append(pkg_filepath)
225 |
226 | # -- STEP: Make local PYTHON PACKAGE INDEX.
227 | root_package = Package(None, "Python Package Index")
228 | root_package.files = [ os.path.join(pkg_rootdir, pkg.name, "index.html")
229 | for pkg in packages ]
230 | make_index_for(root_package, pkg_rootdir)
231 | for package in packages:
232 | index_dir = os.path.join(pkg_rootdir, package.name)
233 | make_index_for(package, index_dir)
234 |
235 |
236 | # -----------------------------------------------------------------------------
237 | # MAIN:
238 | # -----------------------------------------------------------------------------
239 | if __name__ == "__main__":
240 | if (len(sys.argv) != 2) or "-h" in sys.argv[1:] or "--help" in sys.argv[1:]:
241 | print("USAGE: %s DOWNLOAD_DIR" % os.path.basename(sys.argv[0]))
242 | print(__doc__)
243 | sys.exit(1)
244 | make_package_index(sys.argv[1])
245 |
--------------------------------------------------------------------------------
/bin/project_bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # =============================================================================
3 | # BOOTSTRAP PROJECT: Download all requirements
4 | # =============================================================================
5 | # test ${PIP_DOWNLOADS_DIR} || mkdir -p ${PIP_DOWNLOADS_DIR}
6 | # tox -e init
7 |
8 | set -e
9 |
10 | # -- CONFIGURATION:
11 | HERE=`dirname $0`
12 | TOP="${HERE}/.."
13 | : ${PIP_INDEX_URL="http://pypi.python.org/simple"}
14 | : ${PIP_DOWNLOAD_DIR:="${TOP}/downloads"}
15 | export PIP_INDEX_URL PIP_DOWNLOADS_DIR
16 |
17 | # -- EXECUTE STEPS:
18 | ${HERE}/toxcmd.py mkdir ${PIP_DOWNLOAD_DIR}
19 | pip install --download=${PIP_DOWNLOAD_DIR} -r ${TOP}/requirements/all.txt
20 | ${HERE}/make_localpi.py ${PIP_DOWNLOAD_DIR}
21 |
22 |
--------------------------------------------------------------------------------
/bin/toxcmd.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | """
4 | Provides a command container for additional tox commands, used in "tox.ini".
5 |
6 | COMMANDS:
7 |
8 | * copytree
9 | * copy
10 | * py2to3
11 |
12 | REQUIRES:
13 | * argparse
14 | """
15 |
16 | from glob import glob
17 | import argparse
18 | import inspect
19 | import os.path
20 | import shutil
21 | import sys
22 |
23 | __author__ = "Jens Engel"
24 | __copyright__ = "(c) 2013 by Jens Engel"
25 | __license__ = "BSD"
26 |
27 | # -----------------------------------------------------------------------------
28 | # CONSTANTS:
29 | # -----------------------------------------------------------------------------
30 | VERSION = "0.1.0"
31 | FORMATTER_CLASS = argparse.RawDescriptionHelpFormatter
32 |
33 |
34 | # -----------------------------------------------------------------------------
35 | # SUBCOMMAND: copytree
36 | # -----------------------------------------------------------------------------
37 | def command_copytree(args):
38 | """
39 | Copy one or more source directory(s) below a destination directory.
40 | Parts of the destination directory path are created if needed.
41 | Similar to the UNIX command: 'cp -R srcdir destdir'
42 | """
43 | for srcdir in args.srcdirs:
44 | basename = os.path.basename(srcdir)
45 | destdir2 = os.path.normpath(os.path.join(args.destdir, basename))
46 | if os.path.exists(destdir2):
47 | shutil.rmtree(destdir2)
48 | sys.stdout.write("copytree: %s => %s\n" % (srcdir, destdir2))
49 | shutil.copytree(srcdir, destdir2)
50 | return 0
51 |
52 |
53 | def setup_parser_copytree(parser):
54 | parser.add_argument("srcdirs", nargs="+", help="Source directory(s)")
55 | parser.add_argument("destdir", help="Destination directory")
56 |
57 |
58 | command_copytree.usage = "%(prog)s srcdir... destdir"
59 | command_copytree.short = "Copy source dir(s) below a destination directory."
60 | command_copytree.setup_parser = setup_parser_copytree
61 |
62 |
63 | # -----------------------------------------------------------------------------
64 | # SUBCOMMAND: copy
65 | # -----------------------------------------------------------------------------
66 | def command_copy(args):
67 | """
68 | Copy one or more source-files(s) to a destpath (destfile or destdir).
69 | Destdir mode is used if:
70 | * More than one srcfile is provided
71 | * Last parameter ends with a slash ("/").
72 | * Last parameter is an existing directory
73 |
74 | Destination directory path is created if needed.
75 | Similar to the UNIX command: 'cp srcfile... destpath'
76 | """
77 | sources = args.sources
78 | destpath = args.destpath
79 | source_files = []
80 | for file_ in sources:
81 | if "*" in file_:
82 | selected = glob(file_)
83 | source_files.extend(selected)
84 | elif os.path.isfile(file_):
85 | source_files.append(file_)
86 |
87 | if destpath.endswith("/") or os.path.isdir(destpath) or len(sources) > 1:
88 | # -- DESTDIR-MODE: Last argument is a directory.
89 | destdir = destpath
90 | else:
91 | # -- DESTFILE-MODE: Copy (and rename) one file.
92 | assert len(source_files) == 1
93 | destdir = os.path.dirname(destpath)
94 |
95 | # -- WORK-HORSE: Copy one or more files to destpath.
96 | if not os.path.isdir(destdir):
97 | sys.stdout.write("copy: Create dir %s\n" % destdir)
98 | os.makedirs(destdir)
99 | for source in source_files:
100 | destname = os.path.join(destdir, os.path.basename(source))
101 | sys.stdout.write("copy: %s => %s\n" % (source, destname))
102 | shutil.copy(source, destname)
103 | return 0
104 |
105 |
106 | def setup_parser_copy(parser):
107 | parser.add_argument("sources", nargs="+", help="Source files.")
108 | parser.add_argument("destpath", help="Destination path")
109 |
110 |
111 | command_copy.usage = "%(prog)s sources... destpath"
112 | command_copy.short = "Copy one or more source files to a destinition."
113 | command_copy.setup_parser = setup_parser_copy
114 |
115 |
116 | # -----------------------------------------------------------------------------
117 | # SUBCOMMAND: mkdir
118 | # -----------------------------------------------------------------------------
119 | def command_mkdir(args):
120 | """
121 | Create a non-existing directory (or more ...).
122 | If the directory exists, the step is skipped.
123 | Similar to the UNIX command: 'mkdir -p dir'
124 | """
125 | errors = 0
126 | for directory in args.dirs:
127 | if os.path.exists(directory):
128 | if not os.path.isdir(directory):
129 | # -- SANITY CHECK: directory exists, but as file...
130 | sys.stdout.write("mkdir: %s\n" % directory)
131 | sys.stdout.write("ERROR: Exists already, but as file...\n")
132 | errors += 1
133 | else:
134 | # -- NORMAL CASE: Directory does not exits yet.
135 | assert not os.path.isdir(directory)
136 | sys.stdout.write("mkdir: %s\n" % directory)
137 | os.makedirs(directory)
138 | return errors
139 |
140 |
141 | def setup_parser_mkdir(parser):
142 | parser.add_argument("dirs", nargs="+", help="Directory(s)")
143 |
144 | command_mkdir.usage = "%(prog)s dir..."
145 | command_mkdir.short = "Create non-existing directory (or more...)."
146 | command_mkdir.setup_parser = setup_parser_mkdir
147 |
148 | # -----------------------------------------------------------------------------
149 | # SUBCOMMAND: py2to3
150 | # -----------------------------------------------------------------------------
151 | def command_py2to3(args):
152 | """
153 | Apply '2to3' tool (Python2 to Python3 conversion tool) to Python sources.
154 | """
155 | from lib2to3.main import main
156 | sys.exit(main("lib2to3.fixes", args=args.sources))
157 |
158 |
159 | def setup_parser4py2to3(parser):
160 | parser.add_argument("sources", nargs="+", help="Source files.")
161 |
162 |
163 | command_py2to3.name = "2to3"
164 | command_py2to3.usage = "%(prog)s sources..."
165 | command_py2to3.short = "Apply python's 2to3 tool to Python sources."
166 | command_py2to3.setup_parser = setup_parser4py2to3
167 |
168 |
169 | # -----------------------------------------------------------------------------
170 | # COMMAND HELPERS/UTILS:
171 | # -----------------------------------------------------------------------------
172 | def discover_commands():
173 | commands = []
174 | for name, func in inspect.getmembers(inspect.getmodule(toxcmd_main)):
175 | if name.startswith("__"):
176 | continue
177 | if name.startswith("command_") and callable(func):
178 | command_name0 = name.replace("command_", "")
179 | command_name = getattr(func, "name", command_name0)
180 | commands.append(Command(command_name, func))
181 | return commands
182 |
183 |
184 | class Command(object):
185 | def __init__(self, name, func):
186 | assert isinstance(name, basestring)
187 | assert callable(func)
188 | self.name = name
189 | self.func = func
190 | self.parser = None
191 |
192 | def setup_parser(self, command_parser):
193 | setup_parser = getattr(self.func, "setup_parser", None)
194 | if setup_parser and callable(setup_parser):
195 | setup_parser(command_parser)
196 | else:
197 | command_parser.add_argument("args", nargs="*")
198 |
199 | @property
200 | def usage(self):
201 | usage = getattr(self.func, "usage", None)
202 | return usage
203 |
204 | @property
205 | def short_description(self):
206 | short_description = getattr(self.func, "short", "")
207 | return short_description
208 |
209 | @property
210 | def description(self):
211 | return inspect.getdoc(self.func)
212 |
213 | def __call__(self, args):
214 | return self.func(args)
215 |
216 |
217 | # -----------------------------------------------------------------------------
218 | # MAIN-COMMAND:
219 | # -----------------------------------------------------------------------------
220 | def toxcmd_main(args=None):
221 | """Command util with subcommands for tox environments."""
222 | usage = "USAGE: %(prog)s [OPTIONS] COMMAND args..."
223 | if args is None:
224 | args = sys.argv[1:]
225 |
226 | # -- STEP: Build command-line parser.
227 | parser = argparse.ArgumentParser(description=inspect.getdoc(toxcmd_main),
228 | formatter_class=FORMATTER_CLASS)
229 | common_parser = parser.add_argument_group("Common options")
230 | common_parser.add_argument("--version", action="version", version=VERSION)
231 | subparsers = parser.add_subparsers(help="commands")
232 | for command in discover_commands():
233 | command_parser = subparsers.add_parser(command.name,
234 | usage=command.usage,
235 | description=command.description,
236 | help=command.short_description,
237 | formatter_class=FORMATTER_CLASS)
238 | command_parser.set_defaults(func=command)
239 | command.setup_parser(command_parser)
240 | command.parser = command_parser
241 |
242 | # -- STEP: Process command-line and run command.
243 | options = parser.parse_args(args)
244 | command_function = options.func
245 | return command_function(options)
246 |
247 |
248 | # -----------------------------------------------------------------------------
249 | # MAIN:
250 | # -----------------------------------------------------------------------------
251 | if __name__ == "__main__":
252 | sys.exit(toxcmd_main())
253 |
--------------------------------------------------------------------------------
/invoke.yaml:
--------------------------------------------------------------------------------
1 | # =====================================================
2 | # INVOKE CONFIGURATION: parse_type
3 | # =====================================================
4 | # -- ON WINDOWS:
5 | # run:
6 | # echo: true
7 | # pty: false
8 | # shell: C:\Windows\System32\cmd.exe
9 | # =====================================================
10 |
11 | project:
12 | name: parse_type
13 | repo: "pypi"
14 | # -- TODO: until upload problems are resolved.
15 | repo_url: "https://upload.pypi.org/legacy/"
16 |
17 | tasks:
18 | auto_dash_names: false
19 |
20 | run:
21 | echo: true
22 |
23 | cleanup_all:
24 | extra_directories:
25 | - build
26 | - dist
27 | - .hypothesis
28 | - .pytest_cache
29 | - .ruff_cache
30 | - ".venv*"
31 | - ".tox"
32 | extra_files:
33 | - ".done.*"
34 |
--------------------------------------------------------------------------------
/justfile:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # justfile: A makefile-like build script -- parse_type
3 | # =============================================================================
4 | # REQUIRES: cargo install just
5 | # PLATFORMS: Windows, Linux, macOS, ...
6 | # USAGE:
7 | # just --list
8 | # just
9 | # just
10 | #
11 | # SEE ALSO:
12 | # * https://github.com/casey/just
13 | # =============================================================================
14 |
15 | # -- OPTION: Load environment-variables from "$HERE/.env" file (if exists)
16 | set dotenv-load
17 |
18 | # -----------------------------------------------------------------------------
19 | # CONFIG:
20 | # -----------------------------------------------------------------------------
21 | HERE := justfile_directory()
22 | PIP_INSTALL_OPTIONS := env_var_or_default("PIP_INSTALL_OPTIONS", "--quiet")
23 | PYTEST_OPTIONS := env_var_or_default("PYTEST_OPTIONS", "")
24 |
25 |
26 | # -----------------------------------------------------------------------------
27 | # BUILD RECIPES / TARGETS:
28 | # -----------------------------------------------------------------------------
29 |
30 | # DEFAULT-TARGET: Ensure that packages are installed and runs tests.
31 | default: (_ensure-install-packages "testing") test
32 |
33 | # PART=all, testing, ...
34 | install-packages PART="all":
35 | @echo "INSTALL-PACKAGES: {{PART}} ..."
36 | pip install {{PIP_INSTALL_OPTIONS}} -r py.requirements/{{PART}}.txt
37 | @touch "{{HERE}}/.done.install-packages.{{PART}}"
38 |
39 | # ENSURE: Python packages are installed.
40 | _ensure-install-packages PART="all":
41 | #!/usr/bin/env python3
42 | from subprocess import run
43 | from os import path
44 | if not path.exists("{{HERE}}/.done.install-packages.{{PART}}"):
45 | run("just install-packages {{PART}}", shell=True)
46 |
47 | # -- SIMILAR: This solution requires a Bourne-like shell (may not work on: Windows).
48 | # _ensure-install-packages PART="testing":
49 | # @test -e "{{HERE}}/.done.install-packages.{{PART}}" || just install-packages {{PART}}
50 |
51 | # Run tests.
52 | test *TESTS:
53 | python -m pytest {{PYTEST_OPTIONS}} {{TESTS}}
54 |
55 | # Determine test coverage by running the tests.
56 | coverage:
57 | coverage run -m pytest
58 | coverage combine
59 | coverage report
60 | coverage html
61 |
62 | # Cleanup most parts (but leave PRECIOUS parts).
63 | cleanup: (_ensure-install-packages "all")
64 | invoke cleanup
65 |
66 | # Cleanup everything.
67 | cleanup-all:
68 | invoke cleanup.all
69 |
--------------------------------------------------------------------------------
/parse_type/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | # Copyright 2013 - 2023, jenisys
3 | # SPDX-License-Identifier: MIT
4 | """
5 | This module extends the :mod:`parse` to build and derive additional
6 | parse-types from other, existing types.
7 | """
8 |
9 | from __future__ import absolute_import
10 | from parse_type.cardinality import Cardinality
11 | from parse_type.builder import TypeBuilder, build_type_dict
12 |
13 | __all__ = ["Cardinality", "TypeBuilder", "build_type_dict"]
14 |
--------------------------------------------------------------------------------
/parse_type/builder.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # pylint: disable=missing-docstring
3 | r"""
4 | Provides support to compose user-defined parse types.
5 |
6 | Cardinality
7 | ------------
8 |
9 | It is often useful to constrain how often a data type occurs.
10 | This is also called the cardinality of a data type (in a context).
11 | The supported cardinality are:
12 |
13 | * 0..1 zero_or_one, optional: T or None
14 | * 0..N zero_or_more, list_of
15 | * 1..N one_or_more, list_of (many)
16 |
17 |
18 | .. doctest:: cardinality
19 |
20 | >>> from parse_type import TypeBuilder
21 | >>> from parse import Parser
22 |
23 | >>> def parse_number(text):
24 | ... return int(text)
25 | >>> parse_number.pattern = r"\d+"
26 |
27 | >>> parse_many_numbers = TypeBuilder.with_many(parse_number)
28 | >>> more_types = { "Numbers": parse_many_numbers }
29 | >>> parser = Parser("List: {numbers:Numbers}", more_types)
30 | >>> parser.parse("List: 1, 2, 3")
31 |
32 |
33 |
34 | Enumeration Type (Name-to-Value Mappings)
35 | -----------------------------------------
36 |
37 | An Enumeration data type allows to select one of several enum values by using
38 | its name. The converter function returns the selected enum value.
39 |
40 | .. doctest:: make_enum
41 |
42 | >>> parse_enum_yesno = TypeBuilder.make_enum({"yes": True, "no": False})
43 | >>> more_types = { "YesNo": parse_enum_yesno }
44 | >>> parser = Parser("Answer: {answer:YesNo}", more_types)
45 | >>> parser.parse("Answer: yes")
46 |
47 |
48 |
49 | Choice (Name Enumerations)
50 | -----------------------------
51 |
52 | A Choice data type allows to select one of several strings.
53 |
54 | .. doctest:: make_choice
55 |
56 | >>> parse_choice_yesno = TypeBuilder.make_choice(["yes", "no"])
57 | >>> more_types = { "ChoiceYesNo": parse_choice_yesno }
58 | >>> parser = Parser("Answer: {answer:ChoiceYesNo}", more_types)
59 | >>> parser.parse("Answer: yes")
60 |
61 |
62 | """
63 |
64 | from __future__ import absolute_import
65 | import inspect
66 | import re
67 | import enum
68 | from parse_type.cardinality import pattern_group_count, \
69 | Cardinality, TypeBuilder as CardinalityTypeBuilder
70 |
71 | __all__ = ["TypeBuilder", "build_type_dict", "parse_anything"]
72 |
73 |
74 | class TypeBuilder(CardinalityTypeBuilder):
75 | """
76 | Provides a utility class to build type-converters (parse_types) for
77 | the :mod:`parse` module.
78 | """
79 | default_strict = True
80 | default_re_opts = (re.IGNORECASE | re.DOTALL)
81 |
82 | @classmethod
83 | def make_list(cls, item_converter=None, listsep=','):
84 | """
85 | Create a type converter for a list of items (many := 1..*).
86 | The parser accepts anything and the converter needs to fail on errors.
87 |
88 | :param item_converter: Type converter for an item.
89 | :param listsep: List separator to use (as string).
90 | :return: Type converter function object for the list.
91 | """
92 | if not item_converter:
93 | item_converter = parse_anything
94 | return cls.with_cardinality(Cardinality.many, item_converter,
95 | pattern=cls.anything_pattern,
96 | listsep=listsep)
97 |
98 | @staticmethod
99 | def make_enum(enum_mappings):
100 | """
101 | Creates a type converter for an enumeration or text-to-value mapping.
102 |
103 | :param enum_mappings: Defines enumeration names and values.
104 | :return: Type converter function object for the enum/mapping.
105 | """
106 | if (inspect.isclass(enum_mappings) and
107 | issubclass(enum_mappings, enum.Enum)):
108 | enum_class = enum_mappings
109 | enum_mappings = enum_class.__members__
110 |
111 | def convert_enum(text):
112 | if text not in convert_enum.mappings:
113 | text = text.lower() # REQUIRED-BY: parse re.IGNORECASE
114 | return convert_enum.mappings[text] #< text.lower() ???
115 | convert_enum.pattern = r"|".join(enum_mappings.keys())
116 | convert_enum.mappings = enum_mappings
117 | return convert_enum
118 |
119 | @staticmethod
120 | def _normalize_choices(choices, transform):
121 | assert transform is None or callable(transform)
122 | if transform:
123 | choices = [transform(value) for value in choices]
124 | else:
125 | choices = list(choices)
126 | return choices
127 |
128 | @classmethod
129 | def make_choice(cls, choices, transform=None, strict=None):
130 | """
131 | Creates a type-converter function to select one from a list of strings.
132 | The type-converter function returns the selected choice_text.
133 | The :param:`transform()` function is applied in the type converter.
134 | It can be used to enforce the case (because parser uses re.IGNORECASE).
135 |
136 | :param choices: List of strings as choice.
137 | :param transform: Optional, initial transform function for parsed text.
138 | :return: Type converter function object for this choices.
139 | """
140 | # -- NOTE: Parser uses re.IGNORECASE flag
141 | # => transform may enforce case.
142 | choices = cls._normalize_choices(choices, transform)
143 | if strict is None:
144 | strict = cls.default_strict
145 |
146 | def convert_choice(text):
147 | if transform:
148 | text = transform(text)
149 | if strict and text not in convert_choice.choices:
150 | values = ", ".join(convert_choice.choices)
151 | raise ValueError("%s not in: %s" % (text, values))
152 | return text
153 | convert_choice.pattern = r"|".join(choices)
154 | convert_choice.choices = choices
155 | return convert_choice
156 |
157 | @classmethod
158 | def make_choice2(cls, choices, transform=None, strict=None):
159 | """
160 | Creates a type converter to select one item from a list of strings.
161 | The type converter function returns a tuple (index, choice_text).
162 |
163 | :param choices: List of strings as choice.
164 | :param transform: Optional, initial transform function for parsed text.
165 | :return: Type converter function object for this choices.
166 | """
167 | choices = cls._normalize_choices(choices, transform)
168 | if strict is None:
169 | strict = cls.default_strict
170 |
171 | def convert_choice2(text):
172 | if transform:
173 | text = transform(text)
174 | if strict and text not in convert_choice2.choices:
175 | values = ", ".join(convert_choice2.choices)
176 | raise ValueError("%s not in: %s" % (text, values))
177 | index = convert_choice2.choices.index(text)
178 | return index, text
179 | convert_choice2.pattern = r"|".join(choices)
180 | convert_choice2.choices = choices
181 | return convert_choice2
182 |
183 | @classmethod
184 | def make_variant(cls, converters, re_opts=None, compiled=False, strict=True):
185 | """
186 | Creates a type converter for a number of type converter alternatives.
187 | The first matching type converter is used.
188 |
189 | REQUIRES: type_converter.pattern attribute
190 |
191 | :param converters: List of type converters as alternatives.
192 | :param re_opts: Regular expression options zu use (=default_re_opts).
193 | :param compiled: Use compiled regexp matcher, if true (=False).
194 | :param strict: Enable assertion checks.
195 | :return: Type converter function object.
196 |
197 | .. note::
198 |
199 | Works only with named fields in :class:`parse.Parser`.
200 | Parser needs group_index delta for unnamed/fixed fields.
201 | This is not supported for user-defined types.
202 | Otherwise, you need to use :class:`parse_type.parse.Parser`
203 | (patched version of the :mod:`parse` module).
204 | """
205 | # -- NOTE: Uses double-dispatch with regex pattern rematch because
206 | # match is not passed through to primary type converter.
207 | assert converters, "REQUIRE: Non-empty list."
208 | if len(converters) == 1:
209 | return converters[0]
210 | if re_opts is None:
211 | re_opts = cls.default_re_opts
212 |
213 | pattern = r")|(".join([tc.pattern for tc in converters])
214 | pattern = r"("+ pattern + ")"
215 | group_count = len(converters)
216 | for converter in converters:
217 | group_count += pattern_group_count(converter.pattern)
218 |
219 | if compiled:
220 | convert_variant = cls.__create_convert_variant_compiled(converters,
221 | re_opts,
222 | strict)
223 | else:
224 | convert_variant = cls.__create_convert_variant(re_opts, strict)
225 | convert_variant.pattern = pattern
226 | convert_variant.converters = tuple(converters)
227 | convert_variant.regex_group_count = group_count
228 | return convert_variant
229 |
230 | @staticmethod
231 | def __create_convert_variant(re_opts, strict):
232 | # -- USE: Regular expression pattern (compiled on use).
233 | def convert_variant(text, m=None):
234 | # pylint: disable=invalid-name, unused-argument, missing-docstring
235 | for converter in convert_variant.converters:
236 | if re.match(converter.pattern, text, re_opts):
237 | return converter(text)
238 | # -- pragma: no cover
239 | assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
240 | return None
241 | return convert_variant
242 |
243 | @staticmethod
244 | def __create_convert_variant_compiled(converters, re_opts, strict):
245 | # -- USE: Compiled regular expression matcher.
246 | for converter in converters:
247 | matcher = getattr(converter, "matcher", None)
248 | if not matcher:
249 | converter.matcher = re.compile(converter.pattern, re_opts)
250 |
251 | def convert_variant(text, m=None):
252 | # pylint: disable=invalid-name, unused-argument, missing-docstring
253 | for converter in convert_variant.converters:
254 | if converter.matcher.match(text):
255 | return converter(text)
256 | # -- pragma: no cover
257 | assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
258 | return None
259 | return convert_variant
260 |
261 |
262 | def build_type_dict(converters):
263 | """
264 | Builds type dictionary for user-defined type converters,
265 | used by :mod:`parse` module.
266 | This requires that each type converter has a "name" attribute.
267 |
268 | :param converters: List of type converters (parse_types)
269 | :return: Type converter dictionary
270 | """
271 | more_types = {}
272 | for converter in converters:
273 | assert callable(converter)
274 | more_types[converter.name] = converter
275 | return more_types
276 |
277 | # -----------------------------------------------------------------------------
278 | # COMMON TYPE CONVERTERS
279 | # -----------------------------------------------------------------------------
280 | def parse_anything(text, match=None, match_start=0):
281 | """
282 | Provides a generic type converter that accepts anything and returns
283 | the text (unchanged).
284 |
285 | :param text: Text to convert (as string).
286 | :return: Same text (as string).
287 | """
288 | # pylint: disable=unused-argument
289 | return text
290 | parse_anything.pattern = TypeBuilder.anything_pattern
291 |
292 |
293 | # -----------------------------------------------------------------------------
294 | # Copyright (c) 2012-2020 by Jens Engel (https://github/jenisys/parse_type)
295 | #
296 | # Permission is hereby granted, free of charge, to any person obtaining a copy
297 | # of this software and associated documentation files (the "Software"), to deal
298 | # in the Software without restriction, including without limitation the rights
299 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
300 | # copies of the Software, and to permit persons to whom the Software is
301 | # furnished to do so, subject to the following conditions:
302 | #
303 | # The above copyright notice and this permission notice shall be included in
304 | # all copies or substantial portions of the Software.
305 | #
306 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
307 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
308 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
309 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
310 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
311 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
312 | # SOFTWARE.
313 |
--------------------------------------------------------------------------------
/parse_type/cardinality.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | This module simplifies to build parse types and regular expressions
4 | for a data type with the specified cardinality.
5 | """
6 |
7 | # -- USE: enum34
8 | from __future__ import absolute_import
9 | from enum import Enum
10 |
11 |
12 | # -----------------------------------------------------------------------------
13 | # FUNCTIONS:
14 | # -----------------------------------------------------------------------------
15 | def pattern_group_count(pattern):
16 | """Count the pattern-groups within a regex-pattern (as text)."""
17 | return pattern.replace(r"\(", "").count("(")
18 |
19 |
20 | # -----------------------------------------------------------------------------
21 | # CLASS: Cardinality (Enum Class)
22 | # -----------------------------------------------------------------------------
23 | class Cardinality(Enum):
24 | """Cardinality enumeration class to simplify building regular expression
25 | patterns for a data type with the specified cardinality.
26 | """
27 | # pylint: disable=bad-whitespace
28 | __order__ = "one, zero_or_one, zero_or_more, one_or_more"
29 | one = (None, 0)
30 | zero_or_one = (r"(%s)?", 1) # SCHEMA: pattern
31 | zero_or_more = (r"(%s)?(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern
32 | one_or_more = (r"(%s)(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern
33 |
34 | # -- ALIASES:
35 | optional = zero_or_one
36 | many0 = zero_or_more
37 | many = one_or_more
38 |
39 | def __init__(self, schema, group_count=0):
40 | self.schema = schema
41 | self.group_count = group_count #< Number of match groups.
42 |
43 | def is_many(self):
44 | """Checks for a more general interpretation of "many".
45 |
46 | :return: True, if Cardinality.zero_or_more or Cardinality.one_or_more.
47 | """
48 | return ((self is Cardinality.zero_or_more) or
49 | (self is Cardinality.one_or_more))
50 |
51 | def make_pattern(self, pattern, listsep=','):
52 | """Make pattern for a data type with the specified cardinality.
53 |
54 | .. code-block:: python
55 |
56 | yes_no_pattern = r"yes|no"
57 | many_yes_no = Cardinality.one_or_more.make_pattern(yes_no_pattern)
58 |
59 | :param pattern: Regular expression for type (as string).
60 | :param listsep: List separator for multiple items (as string, optional)
61 | :return: Regular expression pattern for type with cardinality.
62 | """
63 | if self is Cardinality.one:
64 | return pattern
65 | elif self is Cardinality.zero_or_one:
66 | return self.schema % pattern
67 | # -- OTHERWISE:
68 | return self.schema % (pattern, listsep, pattern)
69 |
70 | def compute_group_count(self, pattern):
71 | """Compute the number of regexp match groups when the pattern is provided
72 | to the :func:`Cardinality.make_pattern()` method.
73 |
74 | :param pattern: Item regexp pattern (as string).
75 | :return: Number of regexp match groups in the cardinality pattern.
76 | """
77 | group_count = self.group_count
78 | pattern_repeated = 1
79 | if self.is_many():
80 | pattern_repeated = 2
81 | return group_count + pattern_repeated * pattern_group_count(pattern)
82 |
83 |
84 | # -----------------------------------------------------------------------------
85 | # CLASS: TypeBuilder
86 | # -----------------------------------------------------------------------------
87 | class TypeBuilder(object):
88 | """Provides a utility class to build type-converters (parse_types) for parse.
89 | It supports to build new type-converters for different cardinality
90 | based on the type-converter for cardinality one.
91 | """
92 | anything_pattern = r".+?"
93 | default_pattern = anything_pattern
94 |
95 | @classmethod
96 | def with_cardinality(cls, cardinality, converter, pattern=None,
97 | listsep=','):
98 | """Creates a type converter for the specified cardinality
99 | by using the type converter for T.
100 |
101 | :param cardinality: Cardinality to use (0..1, 0..*, 1..*).
102 | :param converter: Type converter (function) for data type T.
103 | :param pattern: Regexp pattern for an item (=converter.pattern).
104 | :return: type-converter for optional (T or None).
105 | """
106 | if cardinality is Cardinality.one:
107 | return converter
108 | # -- NORMAL-CASE
109 | builder_func = getattr(cls, "with_%s" % cardinality.name)
110 | if cardinality is Cardinality.zero_or_one:
111 | return builder_func(converter, pattern)
112 | # -- MANY CASE: 0..*, 1..*
113 | return builder_func(converter, pattern, listsep=listsep)
114 |
115 | @classmethod
116 | def with_zero_or_one(cls, converter, pattern=None):
117 | """Creates a type converter for a T with 0..1 times
118 | by using the type converter for one item of T.
119 |
120 | :param converter: Type converter (function) for data type T.
121 | :param pattern: Regexp pattern for an item (=converter.pattern).
122 | :return: type-converter for optional (T or None).
123 | """
124 | cardinality = Cardinality.zero_or_one
125 | if not pattern:
126 | pattern = getattr(converter, "pattern", cls.default_pattern)
127 | optional_pattern = cardinality.make_pattern(pattern)
128 | group_count = cardinality.compute_group_count(pattern)
129 |
130 | def convert_optional(text, m=None):
131 | # pylint: disable=invalid-name, unused-argument, missing-docstring
132 | if text:
133 | text = text.strip()
134 | if not text:
135 | return None
136 | return converter(text)
137 | convert_optional.pattern = optional_pattern
138 | convert_optional.regex_group_count = group_count
139 | return convert_optional
140 |
141 | @classmethod
142 | def with_zero_or_more(cls, converter, pattern=None, listsep=","):
143 | """Creates a type converter function for a list with 0..N items
144 | by using the type converter for one item of T.
145 |
146 | :param converter: Type converter (function) for data type T.
147 | :param pattern: Regexp pattern for an item (=converter.pattern).
148 | :param listsep: Optional list separator between items (default: ',')
149 | :return: type-converter for list
150 | """
151 | cardinality = Cardinality.zero_or_more
152 | if not pattern:
153 | pattern = getattr(converter, "pattern", cls.default_pattern)
154 | many0_pattern = cardinality.make_pattern(pattern, listsep)
155 | group_count = cardinality.compute_group_count(pattern)
156 |
157 | def convert_list0(text, m=None):
158 | # pylint: disable=invalid-name, unused-argument, missing-docstring
159 | if text:
160 | text = text.strip()
161 | if not text:
162 | return []
163 | return [converter(part.strip()) for part in text.split(listsep)]
164 | convert_list0.pattern = many0_pattern
165 | # OLD convert_list0.group_count = group_count
166 | convert_list0.regex_group_count = group_count
167 | return convert_list0
168 |
169 | @classmethod
170 | def with_one_or_more(cls, converter, pattern=None, listsep=","):
171 | """Creates a type converter function for a list with 1..N items
172 | by using the type converter for one item of T.
173 |
174 | :param converter: Type converter (function) for data type T.
175 | :param pattern: Regexp pattern for an item (=converter.pattern).
176 | :param listsep: Optional list separator between items (default: ',')
177 | :return: Type converter for list
178 | """
179 | cardinality = Cardinality.one_or_more
180 | if not pattern:
181 | pattern = getattr(converter, "pattern", cls.default_pattern)
182 | many_pattern = cardinality.make_pattern(pattern, listsep)
183 | group_count = cardinality.compute_group_count(pattern)
184 |
185 | def convert_list(text, m=None):
186 | # pylint: disable=invalid-name, unused-argument, missing-docstring
187 | return [converter(part.strip()) for part in text.split(listsep)]
188 | convert_list.pattern = many_pattern
189 | # OLD: convert_list.group_count = group_count
190 | convert_list.regex_group_count = group_count
191 | return convert_list
192 |
193 | # -- ALIAS METHODS:
194 | @classmethod
195 | def with_optional(cls, converter, pattern=None):
196 | """Alias for :py:meth:`with_zero_or_one()` method."""
197 | return cls.with_zero_or_one(converter, pattern)
198 |
199 | @classmethod
200 | def with_many(cls, converter, pattern=None, listsep=','):
201 | """Alias for :py:meth:`with_one_or_more()` method."""
202 | return cls.with_one_or_more(converter, pattern, listsep)
203 |
204 | @classmethod
205 | def with_many0(cls, converter, pattern=None, listsep=','):
206 | """Alias for :py:meth:`with_zero_or_more()` method."""
207 | return cls.with_zero_or_more(converter, pattern, listsep)
208 |
--------------------------------------------------------------------------------
/parse_type/cardinality_field.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Provides support for cardinality fields.
4 | A cardinality field is a type suffix for parse format expression, ala:
5 |
6 | "{person:Person?}" #< Cardinality: 0..1 = zero or one = optional
7 | "{persons:Person*}" #< Cardinality: 0..* = zero or more = many0
8 | "{persons:Person+}" #< Cardinality: 1..* = one or more = many
9 | """
10 |
11 | from __future__ import absolute_import
12 | import six
13 | from parse_type.cardinality import Cardinality, TypeBuilder
14 |
15 |
16 | class MissingTypeError(KeyError): # pylint: disable=missing-docstring
17 | pass
18 |
19 | # -----------------------------------------------------------------------------
20 | # CLASS: Cardinality (Field Part)
21 | # -----------------------------------------------------------------------------
22 | class CardinalityField(object):
23 | """Cardinality field for parse format expression, ala:
24 |
25 | "{person:Person?}" #< Cardinality: 0..1 = zero or one = optional
26 | "{persons:Person*}" #< Cardinality: 0..* = zero or more = many0
27 | "{persons:Person+}" #< Cardinality: 1..* = one or more = many
28 | """
29 |
30 | # -- MAPPING SUPPORT:
31 | pattern_chars = "?*+"
32 | from_char_map = {
33 | '?': Cardinality.zero_or_one,
34 | '*': Cardinality.zero_or_more,
35 | '+': Cardinality.one_or_more,
36 | }
37 | to_char_map = dict([(value, key) for key, value in from_char_map.items()])
38 |
39 | @classmethod
40 | def matches_type(cls, type_name):
41 | """Checks if a type name uses the CardinalityField naming scheme.
42 |
43 | :param type_name: Type name to check (as string).
44 | :return: True, if type name has CardinalityField name suffix.
45 | """
46 | return type_name and type_name[-1] in CardinalityField.pattern_chars
47 |
48 | @classmethod
49 | def split_type(cls, type_name):
50 | """Split type of a type name with CardinalityField suffix into its parts.
51 |
52 | :param type_name: Type name (as string).
53 | :return: Tuple (type_basename, cardinality)
54 | """
55 | if cls.matches_type(type_name):
56 | basename = type_name[:-1]
57 | cardinality = cls.from_char_map[type_name[-1]]
58 | else:
59 | # -- ASSUME: Cardinality.one
60 | cardinality = Cardinality.one
61 | basename = type_name
62 | return (basename, cardinality)
63 |
64 | @classmethod
65 | def make_type(cls, basename, cardinality):
66 | """Build new type name according to CardinalityField naming scheme.
67 |
68 | :param basename: Type basename of primary type (as string).
69 | :param cardinality: Cardinality of the new type (as Cardinality item).
70 | :return: Type name with CardinalityField suffix (if needed)
71 | """
72 | if cardinality is Cardinality.one:
73 | # -- POSTCONDITION: assert not cls.make_type(type_name)
74 | return basename
75 | # -- NORMAL CASE: type with CardinalityField suffix.
76 | type_name = "%s%s" % (basename, cls.to_char_map[cardinality])
77 | # -- POSTCONDITION: assert cls.make_type(type_name)
78 | return type_name
79 |
80 |
81 | # -----------------------------------------------------------------------------
82 | # CLASS: CardinalityFieldTypeBuilder
83 | # -----------------------------------------------------------------------------
84 | class CardinalityFieldTypeBuilder(object):
85 | """Utility class to create type converters based on:
86 |
87 | * the CardinalityField naming scheme and
88 | * type converter for cardinality=1
89 | """
90 |
91 | listsep = ','
92 |
93 | @classmethod
94 | def create_type_variant(cls, type_name, type_converter):
95 | r"""Create type variants for types with a cardinality field.
96 | The new type converters are based on the type converter with
97 | cardinality=1.
98 |
99 | .. code-block:: python
100 |
101 | import parse
102 |
103 | @parse.with_pattern(r'\d+')
104 | def parse_number(text):
105 | return int(text)
106 |
107 | new_type = CardinalityFieldTypeBuilder.create_type_variant(
108 | "Number+", parse_number)
109 | new_type = CardinalityFieldTypeBuilder.create_type_variant(
110 | "Number+", dict(Number=parse_number))
111 |
112 | :param type_name: Type name with cardinality field suffix.
113 | :param type_converter: Type converter or type dictionary.
114 | :return: Type converter variant (function).
115 | :raises: ValueError, if type_name does not end with CardinalityField
116 | :raises: MissingTypeError, if type_converter is missing in type_dict
117 | """
118 | assert isinstance(type_name, six.string_types)
119 | if not CardinalityField.matches_type(type_name):
120 | message = "type_name='%s' has no CardinalityField" % type_name
121 | raise ValueError(message)
122 |
123 | primary_name, cardinality = CardinalityField.split_type(type_name)
124 | if isinstance(type_converter, dict):
125 | type_dict = type_converter
126 | type_converter = type_dict.get(primary_name, None)
127 | if not type_converter:
128 | raise MissingTypeError(primary_name)
129 |
130 | assert callable(type_converter)
131 | type_variant = TypeBuilder.with_cardinality(cardinality,
132 | type_converter,
133 | listsep=cls.listsep)
134 | type_variant.name = type_name
135 | return type_variant
136 |
137 |
138 | @classmethod
139 | def create_type_variants(cls, type_names, type_dict):
140 | """Create type variants for types with a cardinality field.
141 | The new type converters are based on the type converter with
142 | cardinality=1.
143 |
144 | .. code-block:: python
145 |
146 | # -- USE: parse_number() type converter function.
147 | new_types = CardinalityFieldTypeBuilder.create_type_variants(
148 | ["Number?", "Number+"], dict(Number=parse_number))
149 |
150 | :param type_names: List of type names with cardinality field suffix.
151 | :param type_dict: Type dictionary with named type converters.
152 | :return: Type dictionary with type converter variants.
153 | """
154 | type_variant_dict = {}
155 | for type_name in type_names:
156 | type_variant = cls.create_type_variant(type_name, type_dict)
157 | type_variant_dict[type_name] = type_variant
158 | return type_variant_dict
159 |
160 | # MAYBE: Check if really needed.
161 | @classmethod
162 | def create_missing_type_variants(cls, type_names, type_dict):
163 | """Create missing type variants for types with a cardinality field.
164 |
165 | :param type_names: List of type names with cardinality field suffix.
166 | :param type_dict: Type dictionary with named type converters.
167 | :return: Type dictionary with missing type converter variants.
168 | """
169 | missing_type_names = [name for name in type_names
170 | if name not in type_dict]
171 | return cls.create_type_variants(missing_type_names, type_dict)
172 |
--------------------------------------------------------------------------------
/parse_type/cfparse.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Provides an extended :class:`parse.Parser` class that supports the
4 | cardinality fields in (user-defined) types.
5 | """
6 |
7 | from __future__ import absolute_import
8 | import logging
9 | import parse
10 | from .cardinality_field import CardinalityField, CardinalityFieldTypeBuilder
11 | from .parse_util import FieldParser
12 |
13 |
14 | log = logging.getLogger(__name__) # pylint: disable=invalid-name
15 |
16 |
17 | class Parser(parse.Parser):
18 | """Provides an extended :class:`parse.Parser` with cardinality field support.
19 | A cardinality field is a type suffix for parse format expression, ala:
20 |
21 | "... {person:Person?} ..." -- OPTIONAL: Cardinality zero or one, 0..1
22 | "... {persons:Person*} ..." -- MANY0: Cardinality zero or more, 0..
23 | "... {persons:Person+} ..." -- MANY: Cardinality one or more, 1..
24 |
25 | When the primary type converter for cardinality=1 is provided,
26 | the type variants for the other cardinality cases can be derived from it.
27 |
28 | This parser class automatically creates missing type variants for types
29 | with a cardinality field and passes the extended type dictionary
30 | to its base class.
31 | """
32 | # -- TYPE-BUILDER: For missing types in Fields with CardinalityField part.
33 | type_builder = CardinalityFieldTypeBuilder
34 |
35 | def __init__(self, schema, extra_types=None, case_sensitive=False,
36 | type_builder=None):
37 | """Creates a parser with CardinalityField part support.
38 |
39 | :param schema: Parse schema (or format) for parser (as string).
40 | :param extra_types: Type dictionary with type converters (or None).
41 | :param case_sensitive: Indicates if case-sensitive regexp are used.
42 | :param type_builder: Type builder to use for missing types.
43 | """
44 | if extra_types is None:
45 | extra_types = {}
46 | missing = self.create_missing_types(schema, extra_types, type_builder)
47 | if missing:
48 | # pylint: disable=logging-not-lazy
49 | log.debug("MISSING TYPES: %s" % ",".join(missing.keys()))
50 | extra_types.update(missing)
51 |
52 | # -- FINALLY: Delegate to base class.
53 | super(Parser, self).__init__(schema, extra_types,
54 | case_sensitive=case_sensitive)
55 |
56 | @classmethod
57 | def create_missing_types(cls, schema, type_dict, type_builder=None):
58 | """Creates missing types for fields with a CardinalityField part.
59 | It is assumed that the primary type converter for cardinality=1
60 | is registered in the type dictionary.
61 |
62 | :param schema: Parse schema (or format) for parser (as string).
63 | :param type_dict: Type dictionary with type converters.
64 | :param type_builder: Type builder to use for missing types.
65 | :return: Type dictionary with missing types. Empty, if none.
66 | :raises: MissingTypeError,
67 | if a primary type converter with cardinality=1 is missing.
68 | """
69 | if not type_builder:
70 | type_builder = cls.type_builder
71 |
72 | missing = cls.extract_missing_special_type_names(schema, type_dict)
73 | return type_builder.create_type_variants(missing, type_dict)
74 |
75 | @staticmethod
76 | def extract_missing_special_type_names(schema, type_dict):
77 | # pylint: disable=invalid-name
78 | """Extract the type names for fields with CardinalityField part.
79 | Selects only the missing type names that are not in the type dictionary.
80 |
81 | :param schema: Parse schema to use (as string).
82 | :param type_dict: Type dictionary with type converters.
83 | :return: Generator with missing type names (as string).
84 | """
85 | for name in FieldParser.extract_types(schema):
86 | if CardinalityField.matches_type(name) and (name not in type_dict):
87 | yield name
88 |
--------------------------------------------------------------------------------
/parse_type/parse_util.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # pylint: disable=missing-docstring
3 | """
4 | Provides generic utility classes for the :class:`parse.Parser` class.
5 | """
6 |
7 | from __future__ import absolute_import
8 | from collections import namedtuple
9 | import parse
10 | import six
11 |
12 |
13 | # -- HELPER-CLASS: For format part in a Field.
14 | # REQUIRES: Python 2.6 or newer.
15 | # pylint: disable=redefined-builtin, too-many-arguments
16 | FormatSpec = namedtuple("FormatSpec",
17 | ["type", "width", "zero", "align", "fill", "precision"])
18 |
19 | def make_format_spec(type=None, width="", zero=False, align=None, fill=None,
20 | precision=None):
21 | return FormatSpec(type, width, zero, align, fill, precision)
22 | # pylint: enable=redefined-builtin
23 |
24 | class Field(object):
25 | """
26 | Provides a ValueObject for a Field in a parse expression.
27 |
28 | Examples:
29 | * "{}"
30 | * "{name}"
31 | * "{:format}"
32 | * "{name:format}"
33 |
34 | Format specification: [[fill]align][0][width][.precision][type]
35 | """
36 | # pylint: disable=redefined-builtin
37 | ALIGN_CHARS = '<>=^'
38 |
39 | def __init__(self, name="", format=None):
40 | self.name = name
41 | self.format = format
42 | self._format_spec = None
43 |
44 | def set_format(self, format):
45 | self.format = format
46 | self._format_spec = None
47 |
48 | @property
49 | def has_format(self):
50 | return bool(self.format)
51 |
52 | @property
53 | def format_spec(self):
54 | if not self._format_spec and self.format:
55 | self._format_spec = self.extract_format_spec(self.format)
56 | return self._format_spec
57 |
58 | def __str__(self):
59 | name = self.name or ""
60 | if self.has_format:
61 | return "{%s:%s}" % (name, self.format)
62 | return "{%s}" % name
63 |
64 | def __eq__(self, other):
65 | if isinstance(other, Field):
66 | format1 = self.format or ""
67 | format2 = other.format or ""
68 | return (self.name == other.name) and (format1 == format2)
69 | elif isinstance(other, six.string_types):
70 | return str(self) == other
71 | else:
72 | raise ValueError(other)
73 |
74 | def __ne__(self, other):
75 | return not self.__eq__(other)
76 |
77 | @staticmethod
78 | def make_format(format_spec):
79 | """Build format string from a format specification.
80 |
81 | :param format_spec: Format specification (as FormatSpec object).
82 | :return: Composed format (as string).
83 | """
84 | fill = ''
85 | align = ''
86 | zero = ''
87 | width = format_spec.width
88 | if format_spec.align:
89 | align = format_spec.align[0]
90 | if format_spec.fill:
91 | fill = format_spec.fill[0]
92 | if format_spec.zero:
93 | zero = '0'
94 |
95 | precision_part = ""
96 | if format_spec.precision:
97 | precision_part = ".%s" % format_spec.precision
98 |
99 | # -- FORMAT-SPEC: [[fill]align][0][width][.precision][type]
100 | return "%s%s%s%s%s%s" % (fill, align, zero, width,
101 | precision_part, format_spec.type)
102 |
103 |
104 | @classmethod
105 | def extract_format_spec(cls, format):
106 | """Pull apart the format: [[fill]align][0][width][.precision][type]"""
107 | # -- BASED-ON: parse.extract_format()
108 | # pylint: disable=redefined-builtin, unsubscriptable-object
109 | if not format:
110 | raise ValueError("INVALID-FORMAT: %s (empty-string)" % format)
111 |
112 | orig_format = format
113 | fill = align = None
114 | if format[0] in cls.ALIGN_CHARS:
115 | align = format[0]
116 | format = format[1:]
117 | elif len(format) > 1 and format[1] in cls.ALIGN_CHARS:
118 | fill = format[0]
119 | align = format[1]
120 | format = format[2:]
121 |
122 | zero = False
123 | if format and format[0] == '0':
124 | zero = True
125 | format = format[1:]
126 |
127 | width = ''
128 | while format:
129 | if not format[0].isdigit():
130 | break
131 | width += format[0]
132 | format = format[1:]
133 |
134 | precision = None
135 | if format.startswith('.'):
136 | # Precision isn't needed but we need to capture it so that
137 | # the ValueError isn't raised.
138 | format = format[1:] # drop the '.'
139 | precision = ''
140 | while format:
141 | if not format[0].isdigit():
142 | break
143 | precision += format[0]
144 | format = format[1:]
145 |
146 | # the rest is the type, if present
147 | type = format
148 | if not type:
149 | raise ValueError("INVALID-FORMAT: %s (without type)" % orig_format)
150 | return FormatSpec(type, width, zero, align, fill, precision)
151 |
152 |
153 | class FieldParser(object):
154 | """
155 | Utility class that parses/extracts fields in parse expressions.
156 | """
157 |
158 | @classmethod
159 | def parse(cls, text):
160 | if not (text.startswith('{') and text.endswith('}')):
161 | message = "FIELD-SCHEMA MISMATCH: text='%s' (missing braces)" % text
162 | raise ValueError(message)
163 |
164 | # first: lose the braces
165 | text = text[1:-1]
166 | if ':' in text:
167 | # -- CASE: Typed field with format.
168 | name, format_ = text.split(':')
169 | else:
170 | name = text
171 | format_ = None
172 | return Field(name, format_)
173 |
174 | @classmethod
175 | def extract_fields(cls, schema):
176 | """Extract fields in a parse expression schema.
177 |
178 | :param schema: Parse expression schema/format to use (as string).
179 | :return: Generator for fields in schema (as Field objects).
180 | """
181 | # -- BASED-ON: parse.Parser._generate_expression()
182 | for part in parse.PARSE_RE.split(schema):
183 | if not part or part == '{{' or part == '}}':
184 | continue
185 | elif part[0] == '{':
186 | # this will be a braces-delimited field to handle
187 | yield cls.parse(part)
188 |
189 | @classmethod
190 | def extract_types(cls, schema):
191 | """Extract types (names) for typed fields (with format/type part).
192 |
193 | :param schema: Parser schema/format to use.
194 | :return: Generator for type names (as string).
195 | """
196 | for field in cls.extract_fields(schema):
197 | if field.has_format:
198 | yield field.format_spec.type
199 |
--------------------------------------------------------------------------------
/py.requirements/all.txt:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # BEHAVE: PYTHON PACKAGE REQUIREMENTS: All requirements
3 | # ============================================================================
4 | # DESCRIPTION:
5 | # pip install -r
6 | #
7 | # SEE ALSO:
8 | # * http://www.pip-installer.org/
9 | # ============================================================================
10 |
11 | -r basic.txt
12 | -r packaging.txt
13 | -r develop.txt
14 | -r testing.txt
15 |
--------------------------------------------------------------------------------
/py.requirements/basic.txt:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # PYTHON PACKAGE REQUIREMENTS: Normal usage/installation (minimal)
3 | # ============================================================================
4 | # DESCRIPTION:
5 | # pip install -r
6 | #
7 | # SEE ALSO:
8 | # * http://www.pip-installer.org/
9 | # ============================================================================
10 |
11 | parse >= 1.18.0; python_version >= '3.0'
12 | parse >= 1.13.1; python_version <= '2.7'
13 | enum34; python_version < '3.4'
14 | six >= 1.15
15 |
--------------------------------------------------------------------------------
/py.requirements/ci.github.testing.txt:
--------------------------------------------------------------------------------
1 | pytest < 5.0; python_version < '3.0'
2 | pytest >= 5.0; python_version >= '3.0'
3 | pytest-html >= 1.19.0
4 |
5 | # -- NEEDED: By some tests (as proof of concept)
6 | # NOTE: path.py-10.1 is required for python2.6
7 | # HINT: path.py => path (python-install-package was renamed for python3)
8 | # DISABLED: path.py >= 11.5.0; python_version < '3.5'
9 | # DISABLED: path >= 13.1.0; python_version >= '3.5'
10 |
11 | -r basic.txt
12 |
--------------------------------------------------------------------------------
/py.requirements/develop.txt:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- For development only
3 | # ============================================================================
4 |
5 | # -- BUILD-SYSTEM SUPPORT: Using invoke
6 | -r ../tasks/py.requirements.txt
7 |
8 | # -- RELEASE MANAGEMENT: Push package to pypi.
9 | twine >= 1.13.0
10 | -r packaging.txt
11 |
12 | # -- PYTHON2/PYTHON3 COMPATIBILITY:
13 | modernize >= 0.5
14 |
15 | # -- PYTHON 3 TYPE HINTS:
16 | typing-extensions; python_version >= '3.8'
17 | typer >= 0.12.5; python_version >= '3.7'
18 |
19 | # -- MULTI-REPO TOOL:
20 | vcstool >= 0.3.0
21 |
22 | # -- LINTERS:
23 | ruff; python_version >= '3.7'
24 | pylint
25 |
26 | # -- TEST SUPPORT: CODE COVERAGE SUPPORT, ...
27 | coverage >= 4.4
28 | pytest-cov
29 |
30 | tox >= 1.8.1,<4.0 # -- HINT: tox >= 4.0 has breaking changes.
31 | virtualenv < 20.22.0; python_version <= '3.6' # -- SUPPORT FOR: Python 2.7, Python <= 3.6
32 | virtualenv >= 20.0.0; python_version > '3.6'
33 | argparse # -- NEEDED-FOR: toxcmd.py
34 |
35 | # -- RELATED:
36 | -r testing.txt
37 | -r docs.txt
38 |
--------------------------------------------------------------------------------
/py.requirements/docs.txt:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # PYTHON PACKAGE REQUIREMENTS: For documentation generation (PREPARED)
3 | # ============================================================================
4 |
5 | Sphinx >=1.6
6 | sphinx_bootstrap_theme >= 0.6.0
7 |
--------------------------------------------------------------------------------
/py.requirements/optional.txt:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- Optional for development
3 | # ============================================================================
4 |
5 | # -- GIT MULTI-REPO TOOL: wstool
6 | # REQUIRES: wstool >= 0.1.18 (which is not in pypi.org, yet)
7 | https://github.com/vcstools/wstool/archive/0.1.18.zip
8 |
--------------------------------------------------------------------------------
/py.requirements/packaging.txt:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # PYTHON PACKAGE REQUIREMENTS: packaging support
3 | # ============================================================================
4 | # DESCRIPTION:
5 | # pip install -r
6 | #
7 | # SEE ALSO:
8 | # * http://www.pip-installer.org/
9 | # ============================================================================
10 |
11 | # -- PACKAGING SUPPORT:
12 | build >= 0.5.1
13 | setuptools
14 | setuptools-scm
15 | wheel
16 |
17 | # -- DISABLED:
18 | # setuptools >= 64.0.0; python_version >= '3.5'
19 | # setuptools < 45.0.0; python_version < '3.5' # DROP: Python2, Python 3.4 support.
20 | # setuptools_scm >= 8.0.0; python_version >= '3.7'
21 | # setuptools_scm < 8.0.0; python_version < '3.7'
22 |
--------------------------------------------------------------------------------
/py.requirements/testing.txt:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- For testing only
3 | # ============================================================================
4 |
5 | pytest < 5.0; python_version < '3.0'
6 | pytest >= 5.0; python_version >= '3.0'
7 | pytest-html >= 1.19.0
8 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # PACKAGING: parse_type
3 | # =============================================================================
4 | # SEE ALSO:
5 | # * https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
6 | # * https://setuptools-scm.readthedocs.io/en/latest/usage/
7 | # * https://pypi.org/classifiers/
8 | # =============================================================================
9 | # PYTHON3: requires = ["setuptools>=64", "setuptools_scm>=8", "wheel"]
10 | [build-system]
11 | requires = ["setuptools", "setuptools_scm", "wheel"]
12 | build-backend = "setuptools.build_meta"
13 |
14 |
15 | [project]
16 | name = "parse_type"
17 | authors = [
18 | {name = "Jens Engel", email = "jenisys@noreply.github.com"},
19 | ]
20 | description = "Simplifies to build parse types based on the parse module"
21 | dynamic = ["version"]
22 | readme = "README.rst"
23 | requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
24 | keywords = ["parse", "parsing"]
25 | license = {text = "MIT"}
26 | classifiers = [
27 | "Development Status :: 5 - Production/Stable",
28 | "Environment :: Console",
29 | "Environment :: Web Environment",
30 | "Intended Audience :: Developers",
31 | "License :: OSI Approved :: MIT License",
32 | "Operating System :: OS Independent",
33 | "Programming Language :: Python :: 2.7",
34 | "Programming Language :: Python :: 3.2",
35 | "Programming Language :: Python :: 3.3",
36 | "Programming Language :: Python :: 3.4",
37 | "Programming Language :: Python :: 3.5",
38 | "Programming Language :: Python :: 3.6",
39 | "Programming Language :: Python :: 3.7",
40 | "Programming Language :: Python :: 3.8",
41 | "Programming Language :: Python :: 3.9",
42 | "Programming Language :: Python :: 3.10",
43 | "Programming Language :: Python :: 3.11",
44 | "Programming Language :: Python :: Implementation :: CPython",
45 | "Programming Language :: Python :: Implementation :: PyPy",
46 | "Topic :: Software Development :: Code Generators",
47 | "Topic :: Software Development :: Libraries :: Python Modules",
48 | ]
49 | dependencies = [
50 | "parse >= 1.18.0; python_version >= '3.0'",
51 | "parse >= 1.13.1; python_version <= '2.7'",
52 | "enum34; python_version < '3.4'",
53 | "six >= 1.15",
54 | ]
55 |
56 |
57 | [project.urls]
58 | Homepage = "https://github.com/jenisys/parse_type"
59 | Download = "https://pypi.org/project/parse_type/"
60 | "Source Code" = "https://github.com/jenisys/parse_type"
61 | "Issue Tracker" = "https://github.com/jenisys/parse_type/issues/"
62 |
63 |
64 | [project.optional-dependencies]
65 | develop = [
66 | # -- DISABLED:
67 | # "setuptools >= 64.0.0; python_version >= '3.5'",
68 | # "setuptools < 45.0.0; python_version < '3.5'", # DROP: Python2, Python 3.4 support.
69 | # "setuptools_scm >= 8.0.0; python_version >= '3.7'",
70 | # "setuptools_scm < 8.0.0; python_version < '3.7'",
71 | "setuptools",
72 | "setuptools-scm",
73 | "wheel",
74 | "build >= 0.5.1",
75 | "twine >= 1.13.0",
76 | "coverage >= 4.4",
77 | "pytest < 5.0; python_version < '3.0'", # >= 4.2
78 | "pytest >= 5.0; python_version >= '3.0'",
79 | "pytest-html >= 1.19.0",
80 | "pytest-cov",
81 | "tox >=2.8,<4.0",
82 | "virtualenv < 20.22.0; python_version <= '3.6'", # -- SUPPORT FOR: Python 2.7, Python <= 3.6
83 | "virtualenv >= 20.0.0; python_version > '3.6'",
84 | "ruff; python_version >= '3.7'",
85 | "pylint",
86 | ]
87 | docs = [
88 | "Sphinx >=1.6",
89 | "sphinx_bootstrap_theme >= 0.6.0"
90 | ]
91 | testing = [
92 | "pytest < 5.0; python_version < '3.0'", # >= 4.2
93 | "pytest >= 5.0; python_version >= '3.0'",
94 | "pytest-html >= 1.19.0",
95 | ]
96 |
97 |
98 | [tool.distutils.bdist_wheel]
99 | universal = true
100 |
101 |
102 | # -----------------------------------------------------------------------------
103 | # PACAKING TOOL SPECIFIC PARTS:
104 | # -----------------------------------------------------------------------------
105 | [tool.setuptools]
106 | platforms = ["any"]
107 | zip-safe = true
108 |
109 | # -- DISABLED:
110 | # [tool.setuptools.dynamic]
111 | # version = {attr = "parse_type._version.version"}
112 |
113 | [tool.setuptools.packages.find]
114 | where = ["."]
115 | include = ["parse_type*"]
116 | exclude = ["tests*"]
117 | namespaces = false
118 |
119 | # -- SETUPTOOLS-SCM: Generate version info from git-tag(s).
120 | [tool.setuptools_scm]
121 | version_file = "parse_type/_version.py"
122 |
123 |
124 | # =============================================================================
125 | # OTHER TOOLS
126 | # =============================================================================
127 | [tool.black]
128 | line_length = 100
129 | target-version = ['py38']
130 | include = '\.pyi?$'
131 | exclude = '''
132 | (
133 | /(
134 | \.git
135 | | \.venv
136 | | \.netbox
137 | | \.vscode
138 | | configuration
139 | )/
140 | )
141 | '''
142 |
143 | [tool.isort]
144 | profile = "black"
145 | multi_line_output = 3
146 | line_length = 100
147 |
148 |
149 | # -----------------------------------------------------------------------------
150 | # PYLINT:
151 | # -----------------------------------------------------------------------------
152 | [tool.pylint.messages_control]
153 | disable = "C0330, C0326"
154 |
155 | [tool.pylint.format]
156 | max-line-length = "100"
157 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # PYTEST CONFIGURATION FILE: pytest.ini
3 | # ============================================================================
4 | # SEE ALSO:
5 | # * http://pytest.org/
6 | # * http://pytest.org/latest/customize.html
7 | # * http://pytest.org/latest/usage.html
8 | # * http://pytest.org/latest/example/pythoncollection.html#change-naming-conventions
9 | # ============================================================================
10 | # MORE OPTIONS:
11 | # addopts =
12 | # python_classes=*Test
13 | # python_functions=test
14 | # ============================================================================
15 |
16 | [pytest]
17 | minversion = 4.2
18 | testpaths = tests
19 | python_files = test_*.py
20 | junit_family = xunit2
21 | addopts = --metadata PACKAGE_UNDER_TEST parse_type
22 | --html=build/testing/report.html --self-contained-html
23 | --junit-xml=build/testing/report.xml
24 |
25 | # markers =
26 | # smoke
27 | # slow
28 |
29 | # -- PREPARED:
30 | # filterwarnings =
31 | # ignore:.*invalid escape sequence.*:DeprecationWarning
32 |
33 | # -- BACKWARD COMPATIBILITY: pytest < 2.8
34 | norecursedirs = .git .tox build dist .venv* tmp* _*
35 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [upload_docs]
2 | upload_dir = build/docs/html
3 |
4 | [bdist_wheel]
5 | universal = 1
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | Setup script for "parse_type" package.
5 |
6 | USAGE:
7 | pip install .
8 |
9 | SEE ALSO:
10 |
11 | * https://pypi.org/pypi/parse_type
12 | * https://github.com/jenisys/parse_type
13 |
14 | RELATED:
15 |
16 | * https://setuptools.readthedocs.io/en/latest/history.html
17 | * https://setuptools-scm.readthedocs.io/en/latest/usage/
18 | """
19 |
20 | import sys
21 | import os.path
22 | sys.path.insert(0, os.curdir)
23 |
24 | # -- USE: setuptools
25 | from setuptools import setup, find_packages
26 | # DISABLED: from setuptools_scm import ScmVersion
27 |
28 |
29 |
30 | # -----------------------------------------------------------------------------
31 | # PREPARE SETUP:
32 | # -----------------------------------------------------------------------------
33 | HERE = os.path.dirname(__file__)
34 | README = os.path.join(HERE, "README.rst")
35 | long_description = ''.join(open(README).readlines()[4:])
36 |
37 |
38 | # -----------------------------------------------------------------------------
39 | # UTILITY:
40 | # -----------------------------------------------------------------------------
41 | def find_packages_by_root_package(where):
42 | """Better than excluding everything that is not needed,
43 | collect only what is needed.
44 | """
45 | root_package = os.path.basename(where)
46 | packages = [ "%s.%s" % (root_package, sub_package)
47 | for sub_package in find_packages(where)]
48 | packages.insert(0, root_package)
49 | return packages
50 |
51 |
52 | # -- SEE: https://setuptools-scm.readthedocs.io/en/latest/customizing/
53 | # HINT: get_version_func(version: ScmVersion) -> str:
54 | def get_this_package_version(version):
55 | from setuptools_scm.version import guess_next_version
56 | if version.distance is None:
57 | # -- FIX: Python 2.7 problem w/ setuptools-scm v5.0.2
58 | version.distance = 0
59 | return version.format_next_version(guess_next_version, "{guessed}b{distance}")
60 |
61 |
62 | # -----------------------------------------------------------------------------
63 | # SETUP:
64 | # -----------------------------------------------------------------------------
65 | setup(
66 | name = "parse_type",
67 | # DISABLED: version = "0.6.3",
68 | use_scm_version={"version_scheme": get_this_package_version},
69 | author = "Jens Engel",
70 | author_email = "jenisys@noreply.github.com",
71 | url = "https://github.com/jenisys/parse_type",
72 | download_url= "http://pypi.python.org/pypi/parse_type",
73 | description = "Simplifies to build parse types based on the parse module",
74 | long_description = long_description,
75 | keywords= "parse, parsing",
76 | license = "MIT",
77 | packages = find_packages_by_root_package("parse_type"),
78 | include_package_data = True,
79 |
80 | # -- REQUIREMENTS:
81 | python_requires=">=2.7, !=3.0.*, !=3.1.*",
82 | setup_requires=[
83 | # -- DISABLED:
84 | # "setuptools >= 64.0.0; python_version >= '3.5'",
85 | # "setuptools < 45.0.0; python_version < '3.5'", # DROP: Python2, Python 3.4 support.
86 | # "setuptools_scm >= 8.0.0; python_version >= '3.7'",
87 | # "setuptools_scm < 8.0.0; python_version < '3.7'",
88 | "setuptools",
89 | "setuptools-scm",
90 | "wheel",
91 | ],
92 | install_requires=[
93 | "parse >= 1.18.0; python_version >= '3.0'",
94 | "parse >= 1.13.1; python_version <= '2.7'",
95 | "enum34; python_version < '3.4'",
96 | "six >= 1.15",
97 | ],
98 | tests_require=[
99 | "pytest < 5.0; python_version < '3.0'", # >= 4.2
100 | "pytest >= 5.0; python_version >= '3.0'",
101 | "pytest-html >= 1.19.0",
102 | ],
103 | extras_require={
104 | "docs": [
105 | "Sphinx >=1.6",
106 | "sphinx_bootstrap_theme >= 0.6.0"
107 | ],
108 | "develop": [
109 | "build >= 0.5.1",
110 | "twine >= 1.13.0",
111 | "coverage >= 4.4",
112 | "pytest < 5.0; python_version < '3.0'", # >= 4.2
113 | "pytest >= 5.0; python_version >= '3.0'",
114 | "pytest-html >= 1.19.0",
115 | "pytest-cov",
116 | "tox >=2.8,<4.0",
117 | "virtualenv < 20.22.0; python_version <= '3.6'", # -- SUPPORT FOR: Python 2.7, Python <= 3.6
118 | "virtualenv >= 20.0.0; python_version > '3.6'",
119 | "ruff; python_version >= '3.7'",
120 | "pylint",
121 | ],
122 | },
123 |
124 | test_suite = "tests",
125 | test_loader = "setuptools.command.test:ScanningLoader",
126 | zip_safe = True,
127 |
128 | classifiers = [
129 | "Development Status :: 5 - Production/Stable",
130 | "Environment :: Console",
131 | "Environment :: Web Environment",
132 | "Intended Audience :: Developers",
133 | "License :: OSI Approved :: MIT License",
134 | "Operating System :: OS Independent",
135 | "Programming Language :: Python :: 2.7",
136 | "Programming Language :: Python :: 3.2",
137 | "Programming Language :: Python :: 3.3",
138 | "Programming Language :: Python :: 3.4",
139 | "Programming Language :: Python :: 3.5",
140 | "Programming Language :: Python :: 3.6",
141 | "Programming Language :: Python :: 3.7",
142 | "Programming Language :: Python :: 3.8",
143 | "Programming Language :: Python :: 3.9",
144 | "Programming Language :: Python :: 3.10",
145 | "Programming Language :: Python :: 3.11",
146 | "Programming Language :: Python :: Implementation :: CPython",
147 | "Programming Language :: Python :: Implementation :: PyPy",
148 | "Topic :: Software Development :: Code Generators",
149 | "Topic :: Software Development :: Libraries :: Python Modules",
150 | ],
151 | platforms = ['any'],
152 | )
153 |
--------------------------------------------------------------------------------
/tasks/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | # pylint: disable=wrong-import-position, wrong-import-order
3 | """
4 | Invoke build script.
5 | Show all tasks with::
6 |
7 | invoke -l
8 |
9 | .. seealso::
10 |
11 | * http://pyinvoke.org
12 | * https://github.com/pyinvoke/invoke
13 | """
14 |
15 | from __future__ import absolute_import, print_function
16 |
17 | # -----------------------------------------------------------------------------
18 | # IMPORTS:
19 | # -----------------------------------------------------------------------------
20 | import sys
21 | from invoke import Collection
22 |
23 | # -- TASK-LIBRARY:
24 | import invoke_cleanup as cleanup
25 | from . import test
26 | from . import release
27 | # DISABLED: from . import docs
28 |
29 | # -----------------------------------------------------------------------------
30 | # TASKS:
31 | # -----------------------------------------------------------------------------
32 | # None
33 |
34 |
35 | # -----------------------------------------------------------------------------
36 | # TASK CONFIGURATION:
37 | # -----------------------------------------------------------------------------
38 | namespace = Collection()
39 | namespace.add_collection(Collection.from_module(cleanup), name="cleanup")
40 | namespace.add_collection(Collection.from_module(test))
41 | namespace.add_collection(Collection.from_module(release))
42 | # -- DISABLED: namespace.add_collection(Collection.from_module(docs))
43 | namespace.configure({
44 | "tasks": {
45 | "auto_dash_names": False
46 | }
47 | })
48 |
49 | # -- ENSURE: python cleanup is used for this project.
50 | cleanup.cleanup_tasks.add_task(cleanup.clean_python)
51 |
52 | # -- INJECT: clean configuration into this namespace
53 | namespace.configure(cleanup.namespace.configuration())
54 | if sys.platform.startswith("win"):
55 | # -- OVERRIDE SETTINGS: For platform=win32, ... (Windows)
56 | from ._compat_shutil import which
57 | run_settings = dict(echo=True, pty=False, shell=which("cmd"))
58 | namespace.configure({"run": run_settings})
59 | else:
60 | namespace.configure({"run": dict(echo=True, pty=True)})
61 |
--------------------------------------------------------------------------------
/tasks/_compat_shutil.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | # pylint: disable=unused-import
3 | # PYTHON VERSION COMPATIBILITY HELPER
4 |
5 | try:
6 | from shutil import which # -- SINCE: Python 3.3
7 | except ImportError:
8 | from backports.shutil_which import which
9 |
--------------------------------------------------------------------------------
/tasks/docs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | """
3 | Provides tasks to build documentation with sphinx, etc.
4 | """
5 |
6 | from __future__ import absolute_import, print_function
7 | import os
8 | import sys
9 | from invoke import task, Collection
10 | from invoke.util import cd
11 | from path import Path
12 |
13 | # -- TASK-LIBRARY:
14 | from ._tasklet_cleanup import cleanup_tasks, cleanup_dirs
15 |
16 |
17 | # -----------------------------------------------------------------------------
18 | # CONSTANTS:
19 | # -----------------------------------------------------------------------------
20 | SPHINX_LANGUAGE_DEFAULT = os.environ.get("SPHINX_LANGUAGE", "en")
21 |
22 |
23 | # -----------------------------------------------------------------------------
24 | # UTILTITIES:
25 | # -----------------------------------------------------------------------------
26 | def _sphinxdoc_get_language(ctx, language=None):
27 | language = language or ctx.config.sphinx.language or SPHINX_LANGUAGE_DEFAULT
28 | return language
29 |
30 |
31 | def _sphinxdoc_get_destdir(ctx, builder, language=None):
32 | if builder == "gettext":
33 | # -- CASE: not LANGUAGE-SPECIFIC
34 | destdir = Path(ctx.config.sphinx.destdir or "build")/builder
35 | else:
36 | # -- CASE: LANGUAGE-SPECIFIC:
37 | language = _sphinxdoc_get_language(ctx, language)
38 | destdir = Path(ctx.config.sphinx.destdir or "build")/builder/language
39 | return destdir
40 |
41 |
42 | # -----------------------------------------------------------------------------
43 | # TASKS:
44 | # -----------------------------------------------------------------------------
45 | @task
46 | def clean(ctx, dry_run=False):
47 | """Cleanup generated document artifacts."""
48 | basedir = ctx.sphinx.destdir or "build/docs"
49 | cleanup_dirs([basedir], dry_run=dry_run)
50 |
51 |
52 | @task(help={
53 | "builder": "Builder to use (html, ...)",
54 | "language": "Language to use (en, ...)",
55 | "options": "Additional options for sphinx-build",
56 | })
57 | def build(ctx, builder="html", language=None, options=""):
58 | """Build docs with sphinx-build"""
59 | language = _sphinxdoc_get_language(ctx, language)
60 | sourcedir = ctx.config.sphinx.sourcedir
61 | destdir = _sphinxdoc_get_destdir(ctx, builder, language=language)
62 | destdir = destdir.abspath()
63 | with cd(sourcedir):
64 | destdir_relative = Path(".").relpathto(destdir)
65 | command = "sphinx-build {opts} -b {builder} -D language={language} {sourcedir} {destdir}" \
66 | .format(builder=builder, sourcedir=".",
67 | destdir=destdir_relative,
68 | language=language,
69 | opts=options)
70 | ctx.run(command)
71 |
72 | @task(help={
73 | "builder": "Builder to use (html, ...)",
74 | "language": "Language to use (en, ...)",
75 | "options": "Additional options for sphinx-build",
76 | })
77 | def rebuild(ctx, builder="html", language=None, options=""):
78 | """Rebuilds the docs.
79 | Perform the steps: clean, build
80 | """
81 | clean(ctx)
82 | build(ctx, builder=builder, language=None, options=options)
83 |
84 | @task
85 | def linkcheck(ctx):
86 | """Check if all links are corect."""
87 | build(ctx, builder="linkcheck")
88 |
89 | @task(help={"language": "Language to use (en, ...)"})
90 | def browse(ctx, language=None):
91 | """Open documentation in web browser."""
92 | output_dir = _sphinxdoc_get_destdir(ctx, "html", language=language)
93 | page_html = Path(output_dir)/"index.html"
94 | if not page_html.exists():
95 | build(ctx, builder="html")
96 | assert page_html.exists()
97 | open_cmd = "open" # -- WORKS ON: MACOSX
98 | if sys.platform.startswith("win"):
99 | open_cmd = "start"
100 | ctx.run("{open} {page_html}".format(open=open_cmd, page_html=page_html))
101 | # ctx.run('python -m webbrowser -t {page_html}'.format(page_html=page_html))
102 | # -- DISABLED:
103 | # import webbrowser
104 | # print("Starting webbrowser with page=%s" % page_html)
105 | # webbrowser.open(str(page_html))
106 |
107 |
108 | @task(help={
109 | "dest": "Destination directory to save docs",
110 | "format": "Format/Builder to use (html, ...)",
111 | "language": "Language to use (en, ...)",
112 | })
113 | # pylint: disable=redefined-builtin
114 | def save(ctx, dest="docs.html", format="html", language=None):
115 | """Save/update docs under destination directory."""
116 | print("STEP: Generate docs in HTML format")
117 | build(ctx, builder=format, language=language)
118 |
119 | print("STEP: Save docs under %s/" % dest)
120 | source_dir = Path(_sphinxdoc_get_destdir(ctx, format, language=language))
121 | Path(dest).rmtree_p()
122 | source_dir.copytree(dest)
123 |
124 | # -- POST-PROCESSING: Polish up.
125 | for part in [".buildinfo", ".doctrees"]:
126 | partpath = Path(dest)/part
127 | if partpath.isdir():
128 | partpath.rmtree_p()
129 | elif partpath.exists():
130 | partpath.remove_p()
131 |
132 |
133 | @task(help={
134 | "language": 'Language to use, like "en" (default: "all" to build all).',
135 | })
136 | def update_translation(ctx, language="all"):
137 | """Update sphinx-doc translation(s) messages from the "English" docs.
138 |
139 | * Generates gettext *.po files in "build/docs/gettext/" directory
140 | * Updates/generates gettext *.po per language in "docs/LOCALE/{language}/"
141 |
142 | .. note:: Afterwards, the missing message translations can be filled in.
143 |
144 | :param language: Indicate which language messages to update (or "all").
145 |
146 | REQUIRES:
147 |
148 | * sphinx
149 | * sphinx-intl >= 0.9
150 |
151 | .. seealso:: https://github.com/sphinx-doc/sphinx-intl
152 | """
153 | if language == "all":
154 | # -- CASE: Process/update all support languages (translations).
155 | DEFAULT_LANGUAGES = os.environ.get("SPHINXINTL_LANGUAGE", None)
156 | if DEFAULT_LANGUAGES:
157 | # -- EXAMPLE: SPHINXINTL_LANGUAGE="de,ja"
158 | DEFAULT_LANGUAGES = DEFAULT_LANGUAGES.split(",")
159 | languages = ctx.config.sphinx.languages or DEFAULT_LANGUAGES
160 | else:
161 | # -- CASE: Process only one language (translation use case).
162 | languages = [language]
163 |
164 | # -- STEP: Generate *.po/*.pot files w/ sphinx-build -b gettext
165 | build(ctx, builder="gettext")
166 |
167 | # -- STEP: Update *.po/*.pot files w/ sphinx-intl
168 | if languages:
169 | gettext_build_dir = _sphinxdoc_get_destdir(ctx, "gettext").abspath()
170 | docs_sourcedir = ctx.config.sphinx.sourcedir
171 | languages_opts = "-l "+ " -l ".join(languages)
172 | with ctx.cd(docs_sourcedir):
173 | ctx.run("sphinx-intl update -p {gettext_dir} {languages}".format(
174 | gettext_dir=gettext_build_dir.relpath(docs_sourcedir),
175 | languages=languages_opts))
176 | else:
177 | print("OOPS: No languages specified (use: SPHINXINTL_LANGUAGE=...)")
178 |
179 |
180 | # -----------------------------------------------------------------------------
181 | # TASK CONFIGURATION:
182 | # -----------------------------------------------------------------------------
183 | namespace = Collection(clean, rebuild, linkcheck, browse, save, update_translation)
184 | namespace.add_task(build, default=True)
185 | namespace.configure({
186 | "sphinx": {
187 | # -- FOR TASKS: docs.build, docs.rebuild, docs.clean, ...
188 | "language": SPHINX_LANGUAGE_DEFAULT,
189 | "sourcedir": "docs",
190 | "destdir": "build/docs",
191 | # -- FOR TASK: docs.update_translation
192 | "languages": None, # -- List of language translations, like: de, ja, ...
193 | }
194 | })
195 |
196 | # -- ADD CLEANUP TASK:
197 | cleanup_tasks.add_task(clean, "clean_docs")
198 | cleanup_tasks.configure(namespace.configuration())
199 |
--------------------------------------------------------------------------------
/tasks/invoke_dry_run.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | """
3 | Basic support to use a --dry-run mode w/ invoke tasks.
4 |
5 | .. code-block::
6 |
7 | from ._dry_run import DryRunContext
8 |
9 | @task
10 | def destroy_something(ctx, path, dry_run=False):
11 | if dry_run:
12 | ctx = DryRunContext(ctx)
13 |
14 | # -- DRY-RUN MODE: Only echos commands.
15 | ctx.run("rm -rf {}".format(path))
16 | """
17 |
18 | from __future__ import print_function
19 | from contextlib import contextmanager
20 |
21 | @contextmanager
22 | def dry_run_mode(ctx):
23 | """Contextmanages/scope-guard that switches into dry-run mode.
24 | Afterwards the original mode is restored.
25 |
26 | .. code-block:: python
27 |
28 | with dry_run_mode(ctx):
29 | ctx.run(...)
30 | """
31 | # -- SETUP PHASE:
32 | initial_dry_run = ctx.config.run.dry
33 | ctx.config.run.dry = True
34 | yield ctx
35 | # -- CLEANUP PHASE:
36 | ctx.config.run.dry = initial_dry_run
37 |
38 |
39 | class DryRunContext(object):
40 | PREFIX = "DRY-RUN: "
41 | SCHEMA = "{prefix}{command}"
42 | SCHEMA_WITH_KWARGS = "{prefix}{command} (with kwargs={kwargs})"
43 |
44 | def __init__(self, ctx=None, prefix=None, schema=None):
45 | if prefix is None:
46 | prefix = self.PREFIX
47 | if schema is None:
48 | schema = self.SCHEMA
49 |
50 | self.ctx = ctx
51 | self.prefix = prefix
52 | self.schema = schema
53 | self.ctx.config.run.dry = True
54 |
55 | @property
56 | def config(self):
57 | return self.ctx.config
58 |
59 | def run(self, command, **kwargs):
60 | message = self.schema.format(command=command,
61 | prefix=self.prefix,
62 | kwargs=kwargs)
63 | print(message)
64 |
65 |
66 | def sudo(self, command, **kwargs):
67 | command2 = "sudo %s" % command
68 | self.run(command2, **kwargs)
69 |
--------------------------------------------------------------------------------
/tasks/py.requirements.txt:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # INVOKE PYTHON PACKAGE REQUIREMENTS: For tasks
3 | # ============================================================================
4 | # DESCRIPTION:
5 | # pip install -r
6 | #
7 | # SEE ALSO:
8 | # * http://www.pip-installer.org/
9 | # ============================================================================
10 |
11 | invoke >=1.7.0,<2.0; python_version < '3.6'
12 | invoke >=1.7.0; python_version >= '3.6'
13 | pycmd
14 | six >= 1.15.0
15 |
16 | # -- HINT, was RENAMED: path.py => path (for python3)
17 | path >= 13.1.0; python_version >= '3.5'
18 | path.py >= 11.5.0; python_version < '3.5'
19 |
20 | # -- PYTHON2 BACKPORTS:
21 | pathlib; python_version <= '3.4'
22 | backports.shutil_which; python_version <= '3.3'
23 |
24 | git+https://github.com/jenisys/invoke-cleanup@v0.3.7
25 |
26 | # -- SECTION: develop
27 | # PREPARED: requests
28 |
--------------------------------------------------------------------------------
/tasks/release.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | """
3 | Tasks for releasing this project.
4 |
5 | Normal steps::
6 |
7 |
8 | python setup.py sdist bdist_wheel
9 |
10 | twine register dist/{project}-{version}.tar.gz
11 | twine upload dist/*
12 |
13 | twine upload --skip-existing dist/*
14 |
15 | python setup.py upload
16 | # -- DEPRECATED: No longer supported -> Use RTD instead
17 | # -- DEPRECATED: python setup.py upload_docs
18 |
19 | pypi repositories:
20 |
21 | * https://pypi.python.org/pypi
22 | * https://testpypi.python.org/pypi (not working anymore)
23 | * https://test.pypi.org/legacy/ (not working anymore)
24 |
25 | Configuration file for pypi repositories:
26 |
27 | .. code-block:: init
28 |
29 | # -- FILE: $HOME/.pypirc
30 | [distutils]
31 | index-servers =
32 | pypi
33 | testpypi
34 |
35 | [pypi]
36 | # DEPRECATED: repository = https://pypi.python.org/pypi
37 | username = __USERNAME_HERE__
38 | password:
39 |
40 | [testpypi]
41 | # DEPRECATED: repository = https://test.pypi.org/legacy
42 | username = __USERNAME_HERE__
43 | password:
44 |
45 | .. seealso::
46 |
47 | * https://packaging.python.org/
48 | * https://packaging.python.org/guides/
49 | * https://packaging.python.org/tutorials/distributing-packages/
50 | """
51 |
52 | from __future__ import absolute_import, print_function
53 | from invoke import Collection, task
54 | from invoke_cleanup import path_glob
55 | from .invoke_dry_run import DryRunContext
56 |
57 |
58 | # -----------------------------------------------------------------------------
59 | # TASKS:
60 | # -----------------------------------------------------------------------------
61 | @task
62 | def checklist(ctx=None): # pylint: disable=unused-argument
63 | """Checklist for releasing this project."""
64 | checklist_text = """PRE-RELEASE CHECKLIST:
65 | [ ] Everything is checked in
66 | [ ] All tests pass w/ tox
67 |
68 | RELEASE CHECKLIST:
69 | [{x1}] Bump version to new-version by adding tag to the repository
70 | [{x2}] Build packages (sdist, bdist_wheel via prepare)
71 | [{x3}] Register and upload packages to testpypi repository (first)
72 | [{x4}] Verify release is OK and packages from testpypi are usable
73 | [{x5}] Register and upload packages to pypi repository
74 | [{x6}] Push last changes to Github repository
75 |
76 | POST-RELEASE CHECKLIST:
77 | [ ] Bump version to new-develop-version by adding tag to the repository
78 | [ ] Adapt CHANGES (if necessary)
79 | [ ] Commit latest changes to Github repository
80 | """
81 | steps = dict(x1=None, x2=None, x3=None, x4=None, x5=None, x6=None)
82 | yesno_map = {True: "x", False: "_", None: " "}
83 | answers = {name: yesno_map[value]
84 | for name, value in steps.items()}
85 | print(checklist_text.format(**answers))
86 |
87 |
88 | @task(name="bump_version")
89 | def bump_version(ctx, new_version, dry_run=False):
90 | """Bump version (to prepare a new release)."""
91 | if not new_version.startswith("v"):
92 | new_version = "v{version}".format(version=new_version)
93 |
94 | if dry_run:
95 | ctx = DryRunContext(ctx)
96 | ctx.run("git tag {version}".format(version=new_version))
97 |
98 |
99 | @task(name="build", aliases=["build_packages"])
100 | def build_packages(ctx, hide=False):
101 | """Build packages for this release."""
102 | print("build_packages:")
103 | ctx.run("python -m build", echo=True, hide=hide)
104 |
105 |
106 | @task
107 | def prepare(ctx, new_version=None, hide=True,
108 | dry_run=False):
109 | """Prepare the release: bump version, build packages, ..."""
110 | if new_version is not None:
111 | bump_version(ctx, new_version, dry_run=dry_run)
112 | build_packages(ctx, hide=hide)
113 | packages = ensure_packages_exist(ctx, check_only=True)
114 | print_packages(packages)
115 |
116 | # -- NOT-NEEDED:
117 | # @task(name="register")
118 | # def register_packages(ctx, repo=None, dry_run=False):
119 | # """Register release (packages) in artifact-store/repository."""
120 | # original_ctx = ctx
121 | # if repo is None:
122 | # repo = ctx.project.repo or "pypi"
123 | # if dry_run:
124 | # ctx = DryRunContext(ctx)
125 |
126 | # packages = ensure_packages_exist(original_ctx)
127 | # print_packages(packages)
128 | # for artifact in packages:
129 | # ctx.run("twine register --repository={repo} {artifact}".format(
130 | # artifact=artifact, repo=repo))
131 |
132 |
133 | @task
134 | def upload(ctx, repo=None, repo_url=None, dry_run=False,
135 | skip_existing=False, verbose=False):
136 | """Upload release packages to repository (artifact-store)."""
137 | if repo is None:
138 | repo = ctx.project.repo or "pypi"
139 | if repo_url is None:
140 | repo_url = ctx.project.repo_url or None
141 | original_ctx = ctx
142 | if dry_run:
143 | ctx = DryRunContext(ctx)
144 |
145 | # -- OPTIONS:
146 | opts = []
147 | if repo_url:
148 | opts.append("--repository-url={0}".format(repo_url))
149 | elif repo:
150 | opts.append("--repository={0}".format(repo))
151 | if skip_existing:
152 | opts.append("--skip-existing")
153 | if verbose:
154 | opts.append("--verbose")
155 |
156 | packages = ensure_packages_exist(original_ctx)
157 | print_packages(packages)
158 | ctx.run("twine upload {opts} dist/*".format(opts=" ".join(opts)))
159 |
160 | # ctx.run("twine upload --repository={repo} dist/*".format(repo=repo))
161 | # 2018-05-05 WORK-AROUND for new https://pypi.org/:
162 | # twine upload --repository-url=https://upload.pypi.org/legacy /dist/*
163 | # NOT-WORKING: repo_url = "https://upload.pypi.org/simple/"
164 | #
165 | # ctx.run("twine upload --repository-url={repo_url} {opts} dist/*".format(
166 | # repo_url=repo_url, opts=" ".join(opts)))
167 | # ctx.run("twine upload --repository={repo} {opts} dist/*".format(
168 | # repo=repo, opts=" ".join(opts)))
169 |
170 |
171 | # -- DEPRECATED: Use RTD instead
172 | # @task(name="upload_docs")
173 | # def upload_docs(ctx, repo=None, dry_run=False):
174 | # """Upload and publish docs.
175 | #
176 | # NOTE: Docs are built first.
177 | # """
178 | # if repo is None:
179 | # repo = ctx.project.repo or "pypi"
180 | # if dry_run:
181 | # ctx = DryRunContext(ctx)
182 | #
183 | # ctx.run("python setup.py upload_docs")
184 | #
185 | # -----------------------------------------------------------------------------
186 | # TASK HELPERS:
187 | # -----------------------------------------------------------------------------
188 | def print_packages(packages):
189 | print("PACKAGES[%d]:" % len(packages))
190 | for package in packages:
191 | package_size = package.stat().st_size
192 | package_time = package.stat().st_mtime
193 | print(" - %s (size=%s)" % (package, package_size))
194 |
195 |
196 | def ensure_packages_exist(ctx, pattern=None, check_only=False):
197 | if pattern is None:
198 | project_name = ctx.project.name
199 | project_prefix = project_name.replace("_", "-").split("-")[0]
200 | pattern = "dist/%s*" % project_prefix
201 |
202 | packages = list(path_glob(pattern, current_dir="."))
203 | if not packages:
204 | if check_only:
205 | message = "No artifacts found: pattern=%s" % pattern
206 | raise RuntimeError(message)
207 | else:
208 | # -- RECURSIVE-SELF-CALL: Once
209 | print("NO-PACKAGES-FOUND: Build packages first ...")
210 | build_packages(ctx, hide=True)
211 | packages = ensure_packages_exist(ctx, pattern,
212 | check_only=True)
213 | return packages
214 |
215 |
216 | # -----------------------------------------------------------------------------
217 | # TASK CONFIGURATION:
218 | # -----------------------------------------------------------------------------
219 | # DISABLED: register_packages
220 | namespace = Collection(bump_version, checklist, prepare, build_packages, upload)
221 | namespace.configure({
222 | "project": {
223 | "repo": "pypi",
224 | "repo_url": None,
225 | }
226 | })
227 |
--------------------------------------------------------------------------------
/tasks/test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | """
3 | Invoke test tasks.
4 | """
5 |
6 | from __future__ import print_function
7 | import os.path
8 | import sys
9 | from invoke import task, Collection
10 |
11 | # -- TASK-LIBRARY:
12 | from invoke_cleanup import cleanup_tasks, cleanup_dirs, cleanup_files
13 |
14 |
15 | # ---------------------------------------------------------------------------
16 | # CONSTANTS:
17 | # ---------------------------------------------------------------------------
18 | USE_BEHAVE = False
19 |
20 |
21 | # ---------------------------------------------------------------------------
22 | # TASKS
23 | # ---------------------------------------------------------------------------
24 | @task(name="all", help={
25 | "args": "Command line args for test run.",
26 | })
27 | def test_all(ctx, args="", options=""):
28 | """Run all tests (default)."""
29 | pytest_args = select_by_prefix(args, ctx.pytest.scopes)
30 | behave_args = None
31 | if USE_BEHAVE:
32 | behave_args = select_by_prefix(args, ctx.behave_test.scopes)
33 | pytest_should_run = not args or (args and pytest_args)
34 | behave_should_run = not args or (args and behave_args)
35 | if pytest_should_run:
36 | pytest(ctx, pytest_args, options=options)
37 | if behave_should_run and USE_BEHAVE:
38 | behave(ctx, behave_args, options=options)
39 |
40 |
41 | @task
42 | def clean(ctx, dry_run=False):
43 | """Cleanup (temporary) test artifacts."""
44 | directories = ctx.test.clean.directories or []
45 | files = ctx.test.clean.files or []
46 | cleanup_dirs(directories, dry_run=dry_run)
47 | cleanup_files(files, dry_run=dry_run)
48 |
49 |
50 | @task(name="unit")
51 | def unittest(ctx, args="", options=""):
52 | """Run unit tests."""
53 | pytest(ctx, args, options)
54 |
55 |
56 | @task
57 | def pytest(ctx, args="", options=""):
58 | """Run unit tests."""
59 | args = args or ctx.pytest.args
60 | options = options or ctx.pytest.options
61 | ctx.run("pytest {options} {args}".format(options=options, args=args))
62 |
63 |
64 | @task(help={
65 | "args": "Command line args for behave",
66 | "format": "Formatter to use (progress, pretty, ...)",
67 | })
68 | def behave(ctx, args="", format="", options=""):
69 | """Run behave tests."""
70 | format = format or ctx.behave_test.format
71 | options = options or ctx.behave_test.options
72 | args = args or ctx.behave_test.args
73 | if os.path.exists("bin/behave"):
74 | behave_cmd = "{python} bin/behave".format(python=sys.executable)
75 | else:
76 | behave_cmd = "{python} -m behave".format(python=sys.executable)
77 |
78 | for group_args in grouped_by_prefix(args, ctx.behave_test.scopes):
79 | ctx.run("{behave} -f {format} {options} {args}".format(
80 | behave=behave_cmd, format=format, options=options, args=group_args))
81 |
82 |
83 | @task(help={
84 | "args": "Tests to run (empty: all)",
85 | "report": "Coverage report format to use (report, html, xml)",
86 | })
87 | def coverage(ctx, args="", report="report", append=False):
88 | """Determine test coverage (run pytest, behave)"""
89 | append = append or ctx.coverage.append
90 | report_formats = ctx.coverage.report_formats or []
91 | if report not in report_formats:
92 | report_formats.insert(0, report)
93 | opts = []
94 | if append:
95 | opts.append("--append")
96 |
97 | pytest_args = select_by_prefix(args, ctx.pytest.scopes)
98 | behave_args = select_by_prefix(args, ctx.behave_test.scopes)
99 | pytest_should_run = not args or (args and pytest_args)
100 | behave_should_run = not args or (args and behave_args) and USE_BEHAVE
101 | if not args:
102 | behave_args = ctx.behave_test.args or "features"
103 | if isinstance(pytest_args, list):
104 | pytest_args = " ".join(pytest_args)
105 | if isinstance(behave_args, list):
106 | behave_args = " ".join(behave_args)
107 |
108 | # -- RUN TESTS WITH COVERAGE:
109 | if pytest_should_run:
110 | ctx.run("coverage run {options} -m pytest {args}".format(
111 | args=pytest_args, options=" ".join(opts)))
112 | if behave_should_run and USE_BEHAVE:
113 | behave_options = ctx.behave_test.coverage_options or ""
114 | os.environ["COVERAGE_PROCESS_START"] = os.path.abspath(".coveragerc")
115 | behave(ctx, args=behave_args, options=behave_options)
116 | del os.environ["COVERAGE_PROCESS_START"]
117 |
118 | # -- POST-PROCESSING:
119 | ctx.run("coverage combine")
120 | for report_format in report_formats:
121 | ctx.run("coverage {report_format}".format(report_format=report_format))
122 |
123 |
124 | # ---------------------------------------------------------------------------
125 | # UTILITIES:
126 | # ---------------------------------------------------------------------------
127 | def select_prefix_for(arg, prefixes):
128 | for prefix in prefixes:
129 | if arg.startswith(prefix):
130 | return prefix
131 | return os.path.dirname(arg)
132 |
133 |
134 | def select_by_prefix(args, prefixes):
135 | selected = []
136 | for arg in args.strip().split():
137 | assert not arg.startswith("-"), "REQUIRE: arg, not options"
138 | scope = select_prefix_for(arg, prefixes)
139 | if scope:
140 | selected.append(arg)
141 | return " ".join(selected)
142 |
143 |
144 | def grouped_by_prefix(args, prefixes):
145 | """Group behave args by (directory) scope into multiple test-runs."""
146 | group_args = []
147 | current_scope = None
148 | for arg in args.strip().split():
149 | assert not arg.startswith("-"), "REQUIRE: arg, not options"
150 | scope = select_prefix_for(arg, prefixes)
151 | if scope != current_scope:
152 | if group_args:
153 | # -- DETECTED GROUP-END:
154 | yield " ".join(group_args)
155 | group_args = []
156 | current_scope = scope
157 | group_args.append(arg)
158 | if group_args:
159 | yield " ".join(group_args)
160 |
161 |
162 | # ---------------------------------------------------------------------------
163 | # TASK MANAGEMENT / CONFIGURATION
164 | # ---------------------------------------------------------------------------
165 | namespace = Collection(clean, unittest, pytest, coverage)
166 | namespace.add_task(test_all, default=True)
167 | if USE_BEHAVE:
168 | namespace.add_task(behave)
169 |
170 | namespace.configure({
171 | "test": {
172 | "clean": {
173 | "directories": [
174 | ".cache", "assets", # -- TEST RUNS
175 | # -- BEHAVE-SPECIFIC:
176 | "__WORKDIR__", "reports", "test_results",
177 | ],
178 | "files": [
179 | ".coverage", ".coverage.*",
180 | # -- BEHAVE-SPECIFIC:
181 | "report.html",
182 | "rerun*.txt", "rerun*.featureset", "testrun*.json",
183 | ],
184 | },
185 | },
186 | "pytest": {
187 | "scopes": ["tests"],
188 | "args": "",
189 | "options": "", # -- NOTE: Overide in configfile "invoke.yaml"
190 | },
191 | # "behave_test": behave.namespace._configuration["behave_test"],
192 | "behave_test": {
193 | "scopes": ["features"],
194 | "args": "features",
195 | "format": "progress",
196 | "options": "", # -- NOTE: Overide in configfile "invoke.yaml"
197 | "coverage_options": "",
198 | },
199 | "coverage": {
200 | "append": False,
201 | "report_formats": ["report", "html"],
202 | },
203 | })
204 |
205 | # -- ADD CLEANUP TASK:
206 | cleanup_tasks.add_task(clean, "clean_test")
207 | cleanup_tasks.configure(namespace.configuration())
208 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenisys/parse_type/35096a3f1c8f1d02b2809a69a68f84fc8d39904b/tests/__init__.py
--------------------------------------------------------------------------------
/tests/parse_tests/__init__.py:
--------------------------------------------------------------------------------
1 | # COPY TESTSUITE FROM: parse v1.20.2
2 | # SEE: https://github.com/r1chardj0n3s/parse
3 |
--------------------------------------------------------------------------------
/tests/parse_tests/test_bugs.py:
--------------------------------------------------------------------------------
1 | import pickle
2 | from datetime import datetime
3 |
4 | import parse
5 |
6 |
7 | def test_tz_compare_to_None():
8 | utc = parse.FixedTzOffset(0, "UTC")
9 | assert utc is not None
10 | assert utc != "spam"
11 |
12 |
13 | def test_named_date_issue7():
14 | r = parse.parse("on {date:ti}", "on 2012-09-17")
15 | assert r["date"] == datetime(2012, 9, 17, 0, 0, 0)
16 |
17 | # fix introduced regressions
18 | r = parse.parse("a {:ti} b", "a 1997-07-16T19:20 b")
19 | assert r[0] == datetime(1997, 7, 16, 19, 20, 0)
20 | r = parse.parse("a {:ti} b", "a 1997-07-16T19:20Z b")
21 | utc = parse.FixedTzOffset(0, "UTC")
22 | assert r[0] == datetime(1997, 7, 16, 19, 20, tzinfo=utc)
23 | r = parse.parse("a {date:ti} b", "a 1997-07-16T19:20Z b")
24 | assert r["date"] == datetime(1997, 7, 16, 19, 20, tzinfo=utc)
25 |
26 |
27 | def test_dotted_type_conversion_pull_8():
28 | # test pull request 8 which fixes type conversion related to dotted
29 | # names being applied correctly
30 | r = parse.parse("{a.b:d}", "1")
31 | assert r["a.b"] == 1
32 | r = parse.parse("{a_b:w} {a.b:d}", "1 2")
33 | assert r["a_b"] == "1"
34 | assert r["a.b"] == 2
35 |
36 |
37 | def test_pm_overflow_issue16():
38 | r = parse.parse("Meet at {:tg}", "Meet at 1/2/2011 12:45 PM")
39 | assert r[0] == datetime(2011, 2, 1, 12, 45)
40 |
41 |
42 | def test_pm_handling_issue57():
43 | r = parse.parse("Meet at {:tg}", "Meet at 1/2/2011 12:15 PM")
44 | assert r[0] == datetime(2011, 2, 1, 12, 15)
45 | r = parse.parse("Meet at {:tg}", "Meet at 1/2/2011 12:15 AM")
46 | assert r[0] == datetime(2011, 2, 1, 0, 15)
47 |
48 |
49 | def test_user_type_with_group_count_issue60():
50 | @parse.with_pattern(r"((\w+))", regex_group_count=2)
51 | def parse_word_and_covert_to_uppercase(text):
52 | return text.strip().upper()
53 |
54 | @parse.with_pattern(r"\d+")
55 | def parse_number(text):
56 | return int(text)
57 |
58 | # -- CASE: Use named (OK)
59 | type_map = {"Name": parse_word_and_covert_to_uppercase, "Number": parse_number}
60 | r = parse.parse(
61 | "Hello {name:Name} {number:Number}", "Hello Alice 42", extra_types=type_map
62 | )
63 | assert r.named == {"name": "ALICE", "number": 42}
64 |
65 | # -- CASE: Use unnamed/fixed (problematic)
66 | r = parse.parse("Hello {:Name} {:Number}", "Hello Alice 42", extra_types=type_map)
67 | assert r[0] == "ALICE"
68 | assert r[1] == 42
69 |
70 |
71 | def test_unmatched_brace_doesnt_match():
72 | r = parse.parse("{who.txt", "hello")
73 | assert r is None
74 |
75 |
76 | def test_pickling_bug_110():
77 | p = parse.compile("{a:d}")
78 | # prior to the fix, this would raise an AttributeError
79 | pickle.dumps(p)
80 |
81 |
82 | def test_unused_centered_alignment_bug():
83 | r = parse.parse("{:^2S}", "foo")
84 | assert r[0] == "foo"
85 | r = parse.search("{:^2S}", "foo")
86 | assert r[0] == "foo"
87 |
88 | # specifically test for the case in issue #118 as well
89 | r = parse.parse("Column {:d}:{:^}", "Column 1: Timestep")
90 | assert r[0] == 1
91 | assert r[1] == "Timestep"
92 |
93 |
94 | def test_unused_left_alignment_bug():
95 | r = parse.parse("{:<2S}", "foo")
96 | assert r[0] == "foo"
97 | r = parse.search("{:<2S}", "foo")
98 | assert r[0] == "foo"
99 |
100 |
101 | def test_match_trailing_newline():
102 | r = parse.parse("{}", "test\n")
103 | assert r[0] == "test\n"
104 |
--------------------------------------------------------------------------------
/tests/parse_tests/test_findall.py:
--------------------------------------------------------------------------------
1 | import parse
2 |
3 |
4 | def test_findall():
5 | s = "".join(
6 | r.fixed[0] for r in parse.findall(">{}<", "some bold text
")
7 | )
8 | assert s == "some bold text"
9 |
10 |
11 | def test_no_evaluate_result():
12 | s = "".join(
13 | m.evaluate_result().fixed[0]
14 | for m in parse.findall(
15 | ">{}<", "some bold text
", evaluate_result=False
16 | )
17 | )
18 | assert s == "some bold text"
19 |
20 |
21 | def test_case_sensitivity():
22 | l = [r.fixed[0] for r in parse.findall("x({})x", "X(hi)X")]
23 | assert l == ["hi"]
24 |
25 | l = [r.fixed[0] for r in parse.findall("x({})x", "X(hi)X", case_sensitive=True)]
26 | assert l == []
27 |
--------------------------------------------------------------------------------
/tests/parse_tests/test_parsetype.py:
--------------------------------------------------------------------------------
1 | from decimal import Decimal
2 |
3 | import pytest
4 |
5 | import parse
6 |
7 |
8 | def assert_match(parser, text, param_name, expected):
9 | result = parser.parse(text)
10 | assert result[param_name] == expected
11 |
12 |
13 | def assert_mismatch(parser, text, param_name):
14 | result = parser.parse(text)
15 | assert result is None
16 |
17 |
18 | def assert_fixed_match(parser, text, expected):
19 | result = parser.parse(text)
20 | assert result.fixed == expected
21 |
22 |
23 | def assert_fixed_mismatch(parser, text):
24 | result = parser.parse(text)
25 | assert result is None
26 |
27 |
28 | def test_pattern_should_be_used():
29 | def parse_number(text):
30 | return int(text)
31 |
32 | parse_number.pattern = r"\d+"
33 | parse_number.name = "Number" # For testing only.
34 |
35 | extra_types = {parse_number.name: parse_number}
36 | format = "Value is {number:Number} and..."
37 | parser = parse.Parser(format, extra_types)
38 |
39 | assert_match(parser, "Value is 42 and...", "number", 42)
40 | assert_match(parser, "Value is 00123 and...", "number", 123)
41 | assert_mismatch(parser, "Value is ALICE and...", "number")
42 | assert_mismatch(parser, "Value is -123 and...", "number")
43 |
44 |
45 | def test_pattern_should_be_used2():
46 | def parse_yesno(text):
47 | return parse_yesno.mapping[text.lower()]
48 |
49 | parse_yesno.mapping = {
50 | "yes": True,
51 | "no": False,
52 | "on": True,
53 | "off": False,
54 | "true": True,
55 | "false": False,
56 | }
57 | parse_yesno.pattern = r"|".join(parse_yesno.mapping.keys())
58 | parse_yesno.name = "YesNo" # For testing only.
59 |
60 | extra_types = {parse_yesno.name: parse_yesno}
61 | format = "Answer: {answer:YesNo}"
62 | parser = parse.Parser(format, extra_types)
63 |
64 | # -- ENSURE: Known enum values are correctly extracted.
65 | for value_name, value in parse_yesno.mapping.items():
66 | text = "Answer: %s" % value_name
67 | assert_match(parser, text, "answer", value)
68 |
69 | # -- IGNORE-CASE: In parsing, calls type converter function !!!
70 | assert_match(parser, "Answer: YES", "answer", True)
71 | assert_mismatch(parser, "Answer: __YES__", "answer")
72 |
73 |
74 | def test_with_pattern():
75 | ab_vals = {"a": 1, "b": 2}
76 |
77 | @parse.with_pattern(r"[ab]")
78 | def ab(text):
79 | return ab_vals[text]
80 |
81 | parser = parse.Parser("test {result:ab}", {"ab": ab})
82 | assert_match(parser, "test a", "result", 1)
83 | assert_match(parser, "test b", "result", 2)
84 | assert_mismatch(parser, "test c", "result")
85 |
86 |
87 | def test_with_pattern_and_regex_group_count():
88 | # -- SPECIAL-CASE: Regex-grouping is used in user-defined type
89 | # NOTE: Missing or wroung regex_group_counts cause problems
90 | # with parsing following params.
91 | @parse.with_pattern(r"(meter|kilometer)", regex_group_count=1)
92 | def parse_unit(text):
93 | return text.strip()
94 |
95 | @parse.with_pattern(r"\d+")
96 | def parse_number(text):
97 | return int(text)
98 |
99 | type_converters = {"Number": parse_number, "Unit": parse_unit}
100 | # -- CASE: Unnamed-params (affected)
101 | parser = parse.Parser("test {:Unit}-{:Number}", type_converters)
102 | assert_fixed_match(parser, "test meter-10", ("meter", 10))
103 | assert_fixed_match(parser, "test kilometer-20", ("kilometer", 20))
104 | assert_fixed_mismatch(parser, "test liter-30")
105 |
106 | # -- CASE: Named-params (uncritical; should not be affected)
107 | # REASON: Named-params have additional, own grouping.
108 | parser2 = parse.Parser("test {unit:Unit}-{value:Number}", type_converters)
109 | assert_match(parser2, "test meter-10", "unit", "meter")
110 | assert_match(parser2, "test meter-10", "value", 10)
111 | assert_match(parser2, "test kilometer-20", "unit", "kilometer")
112 | assert_match(parser2, "test kilometer-20", "value", 20)
113 | assert_mismatch(parser2, "test liter-30", "unit")
114 |
115 |
116 | def test_with_pattern_and_wrong_regex_group_count_raises_error():
117 | # -- SPECIAL-CASE:
118 | # Regex-grouping is used in user-defined type, but wrong value is provided.
119 | @parse.with_pattern(r"(meter|kilometer)", regex_group_count=1)
120 | def parse_unit(text):
121 | return text.strip()
122 |
123 | @parse.with_pattern(r"\d+")
124 | def parse_number(text):
125 | return int(text)
126 |
127 | # -- CASE: Unnamed-params (affected)
128 | BAD_REGEX_GROUP_COUNTS_AND_ERRORS = [
129 | (None, ValueError),
130 | (0, ValueError),
131 | (2, IndexError),
132 | ]
133 | for bad_regex_group_count, error_class in BAD_REGEX_GROUP_COUNTS_AND_ERRORS:
134 | parse_unit.regex_group_count = bad_regex_group_count # -- OVERRIDE-HERE
135 | type_converters = {"Number": parse_number, "Unit": parse_unit}
136 | parser = parse.Parser("test {:Unit}-{:Number}", type_converters)
137 | with pytest.raises(error_class):
138 | parser.parse("test meter-10")
139 |
140 |
141 | def test_with_pattern_and_regex_group_count_is_none():
142 | # -- CORNER-CASE: Increase code-coverage.
143 | data_values = {"a": 1, "b": 2}
144 |
145 | @parse.with_pattern(r"[ab]")
146 | def parse_data(text):
147 | return data_values[text]
148 |
149 | parse_data.regex_group_count = None # ENFORCE: None
150 |
151 | # -- CASE: Unnamed-params
152 | parser = parse.Parser("test {:Data}", {"Data": parse_data})
153 | assert_fixed_match(parser, "test a", (1,))
154 | assert_fixed_match(parser, "test b", (2,))
155 | assert_fixed_mismatch(parser, "test c")
156 |
157 | # -- CASE: Named-params
158 | parser2 = parse.Parser("test {value:Data}", {"Data": parse_data})
159 | assert_match(parser2, "test a", "value", 1)
160 | assert_match(parser2, "test b", "value", 2)
161 | assert_mismatch(parser2, "test c", "value")
162 |
163 |
164 | def test_case_sensitivity():
165 | r = parse.parse("SPAM {} SPAM", "spam spam spam")
166 | assert r[0] == "spam"
167 | assert parse.parse("SPAM {} SPAM", "spam spam spam", case_sensitive=True) is None
168 |
169 |
170 | def test_decimal_value():
171 | value = Decimal("5.5")
172 | str_ = "test {}".format(value)
173 | parser = parse.Parser("test {:F}")
174 | assert parser.parse(str_)[0] == value
175 |
176 |
177 | def test_width_str():
178 | res = parse.parse("{:.2}{:.2}", "look")
179 | assert res.fixed == ("lo", "ok")
180 | res = parse.parse("{:2}{:2}", "look")
181 | assert res.fixed == ("lo", "ok")
182 | res = parse.parse("{:4}{}", "look at that")
183 | assert res.fixed == ("look", " at that")
184 |
185 |
186 | def test_width_constraints():
187 | res = parse.parse("{:4}", "looky")
188 | assert res.fixed == ("looky",)
189 | res = parse.parse("{:4.4}", "looky")
190 | assert res is None
191 | res = parse.parse("{:4.4}", "ook")
192 | assert res is None
193 | res = parse.parse("{:4}{:.4}", "look at that")
194 | assert res.fixed == ("look at ", "that")
195 |
196 |
197 | def test_width_multi_int():
198 | res = parse.parse("{:02d}{:02d}", "0440")
199 | assert res.fixed == (4, 40)
200 | res = parse.parse("{:03d}{:d}", "04404")
201 | assert res.fixed == (44, 4)
202 |
203 |
204 | def test_width_empty_input():
205 | res = parse.parse("{:.2}", "")
206 | assert res is None
207 | res = parse.parse("{:2}", "l")
208 | assert res is None
209 | res = parse.parse("{:2d}", "")
210 | assert res is None
211 |
212 |
213 | def test_int_convert_stateless_base():
214 | parser = parse.Parser("{:d}")
215 | assert parser.parse("1234")[0] == 1234
216 | assert parser.parse("0b1011")[0] == 0b1011
217 |
--------------------------------------------------------------------------------
/tests/parse_tests/test_pattern.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import parse
4 |
5 |
6 | def _test_expression(format, expression):
7 | assert parse.Parser(format)._expression == expression
8 |
9 |
10 | def test_braces():
11 | # pull a simple string out of another string
12 | _test_expression("{{ }}", r"\{ \}")
13 |
14 |
15 | def test_fixed():
16 | # pull a simple string out of another string
17 | _test_expression("{}", r"(.+?)")
18 | _test_expression("{} {}", r"(.+?) (.+?)")
19 |
20 |
21 | def test_named():
22 | # pull a named string out of another string
23 | _test_expression("{name}", r"(?P.+?)")
24 | _test_expression("{name} {other}", r"(?P.+?) (?P.+?)")
25 |
26 |
27 | def test_named_typed():
28 | # pull a named string out of another string
29 | _test_expression("{name:w}", r"(?P\w+)")
30 | _test_expression("{name:w} {other:w}", r"(?P\w+) (?P\w+)")
31 |
32 |
33 | def test_numbered():
34 | _test_expression("{0}", r"(.+?)")
35 | _test_expression("{0} {1}", r"(.+?) (.+?)")
36 | _test_expression("{0:f} {1:f}", r"([-+ ]?\d*\.\d+) ([-+ ]?\d*\.\d+)")
37 |
38 |
39 | def test_bird():
40 | # skip some trailing whitespace
41 | _test_expression("{:>}", r" *(.+?)")
42 |
43 |
44 | def test_format_variety():
45 | def _(fmt, matches):
46 | d = parse.extract_format(fmt, {"spam": "spam"})
47 | for k in matches:
48 | assert d.get(k) == matches[k]
49 |
50 | for t in "%obxegfdDwWsS":
51 | _(t, {"type": t})
52 | _("10" + t, {"type": t, "width": "10"})
53 | _("05d", {"type": "d", "width": "5", "zero": True})
54 | _("<", {"align": "<"})
55 | _(".<", {"align": "<", "fill": "."})
56 | _(">", {"align": ">"})
57 | _(".>", {"align": ">", "fill": "."})
58 | _("^", {"align": "^"})
59 | _(".^", {"align": "^", "fill": "."})
60 | _("x=d", {"type": "d", "align": "=", "fill": "x"})
61 | _("d", {"type": "d"})
62 | _("ti", {"type": "ti"})
63 | _("spam", {"type": "spam"})
64 |
65 | _(".^010d", {"type": "d", "width": "10", "align": "^", "fill": ".", "zero": True})
66 | _(".2f", {"type": "f", "precision": "2"})
67 | _("10.2f", {"type": "f", "width": "10", "precision": "2"})
68 |
69 |
70 | def test_dot_separated_fields():
71 | # this should just work and provide the named value
72 | res = parse.parse("{hello.world}_{jojo.foo.baz}_{simple}", "a_b_c")
73 | assert res.named["hello.world"] == "a"
74 | assert res.named["jojo.foo.baz"] == "b"
75 | assert res.named["simple"] == "c"
76 |
77 |
78 | def test_dict_style_fields():
79 | res = parse.parse("{hello[world]}_{hello[foo][baz]}_{simple}", "a_b_c")
80 | assert res.named["hello"]["world"] == "a"
81 | assert res.named["hello"]["foo"]["baz"] == "b"
82 | assert res.named["simple"] == "c"
83 |
84 |
85 | def test_dot_separated_fields_name_collisions():
86 | # this should just work and provide the named value
87 | res = parse.parse("{a_.b}_{a__b}_{a._b}_{a___b}", "a_b_c_d")
88 | assert res.named["a_.b"] == "a"
89 | assert res.named["a__b"] == "b"
90 | assert res.named["a._b"] == "c"
91 | assert res.named["a___b"] == "d"
92 |
93 |
94 | def test_invalid_groupnames_are_handled_gracefully():
95 | with pytest.raises(NotImplementedError):
96 | parse.parse("{hello['world']}", "doesn't work")
97 |
--------------------------------------------------------------------------------
/tests/parse_tests/test_result.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import parse
4 |
5 |
6 | def test_fixed_access():
7 | r = parse.Result((1, 2), {}, None)
8 | assert r[0] == 1
9 | assert r[1] == 2
10 | with pytest.raises(IndexError):
11 | r[2]
12 | with pytest.raises(KeyError):
13 | r["spam"]
14 |
15 |
16 | def test_slice_access():
17 | r = parse.Result((1, 2, 3, 4), {}, None)
18 | assert r[1:3] == (2, 3)
19 | assert r[-5:5] == (1, 2, 3, 4)
20 | assert r[:4:2] == (1, 3)
21 | assert r[::-2] == (4, 2)
22 | assert r[5:10] == ()
23 |
24 |
25 | def test_named_access():
26 | r = parse.Result((), {"spam": "ham"}, None)
27 | assert r["spam"] == "ham"
28 | with pytest.raises(KeyError):
29 | r["ham"]
30 | with pytest.raises(IndexError):
31 | r[0]
32 |
33 |
34 | def test_contains():
35 | r = parse.Result(("cat",), {"spam": "ham"}, None)
36 | assert "spam" in r
37 | assert "cat" not in r
38 | assert "ham" not in r
39 |
--------------------------------------------------------------------------------
/tests/parse_tests/test_search.py:
--------------------------------------------------------------------------------
1 | import parse
2 |
3 |
4 | def test_basic():
5 | r = parse.search("a {} c", " a b c ")
6 | assert r.fixed == ("b",)
7 |
8 |
9 | def test_multiline():
10 | r = parse.search("age: {:d}\n", "name: Rufus\nage: 42\ncolor: red\n")
11 | assert r.fixed == (42,)
12 |
13 |
14 | def test_pos():
15 | r = parse.search("a {} c", " a b c ", 2)
16 | assert r is None
17 |
18 |
19 | def test_no_evaluate_result():
20 | match = parse.search(
21 | "age: {:d}\n", "name: Rufus\nage: 42\ncolor: red\n", evaluate_result=False
22 | )
23 | r = match.evaluate_result()
24 | assert r.fixed == (42,)
25 |
--------------------------------------------------------------------------------
/tests/parse_tests_with_parse_type/__init__.py:
--------------------------------------------------------------------------------
1 | # COPY TESTSUITE FROM: parse v1.20.2
2 | # SEE: https://github.com/r1chardj0n3s/parse
3 | # NOTES:
4 | # * Apply testsuite to "parse_type.parse"
5 |
--------------------------------------------------------------------------------
/tests/parse_tests_with_parse_type/test_bugs.py:
--------------------------------------------------------------------------------
1 | # -- REPLACE: parse with parse_type.parse
2 | from __future__ import absolute_import, print_function
3 | from parse_type import parse
4 |
5 |
6 | # -- ORGINAL_SOURCE_STARTS_HERE:
7 | import pickle
8 | from datetime import datetime
9 |
10 | # DISABLED: import parse
11 |
12 | def test_tz_compare_to_None():
13 | utc = parse.FixedTzOffset(0, "UTC")
14 | assert utc is not None
15 | assert utc != "spam"
16 |
17 |
18 | def test_named_date_issue7():
19 | r = parse.parse("on {date:ti}", "on 2012-09-17")
20 | assert r["date"] == datetime(2012, 9, 17, 0, 0, 0)
21 |
22 | # fix introduced regressions
23 | r = parse.parse("a {:ti} b", "a 1997-07-16T19:20 b")
24 | assert r[0] == datetime(1997, 7, 16, 19, 20, 0)
25 | r = parse.parse("a {:ti} b", "a 1997-07-16T19:20Z b")
26 | utc = parse.FixedTzOffset(0, "UTC")
27 | assert r[0] == datetime(1997, 7, 16, 19, 20, tzinfo=utc)
28 | r = parse.parse("a {date:ti} b", "a 1997-07-16T19:20Z b")
29 | assert r["date"] == datetime(1997, 7, 16, 19, 20, tzinfo=utc)
30 |
31 |
32 | def test_dotted_type_conversion_pull_8():
33 | # test pull request 8 which fixes type conversion related to dotted
34 | # names being applied correctly
35 | r = parse.parse("{a.b:d}", "1")
36 | assert r["a.b"] == 1
37 | r = parse.parse("{a_b:w} {a.b:d}", "1 2")
38 | assert r["a_b"] == "1"
39 | assert r["a.b"] == 2
40 |
41 |
42 | def test_pm_overflow_issue16():
43 | r = parse.parse("Meet at {:tg}", "Meet at 1/2/2011 12:45 PM")
44 | assert r[0] == datetime(2011, 2, 1, 12, 45)
45 |
46 |
47 | def test_pm_handling_issue57():
48 | r = parse.parse("Meet at {:tg}", "Meet at 1/2/2011 12:15 PM")
49 | assert r[0] == datetime(2011, 2, 1, 12, 15)
50 | r = parse.parse("Meet at {:tg}", "Meet at 1/2/2011 12:15 AM")
51 | assert r[0] == datetime(2011, 2, 1, 0, 15)
52 |
53 |
54 | def test_user_type_with_group_count_issue60():
55 | @parse.with_pattern(r"((\w+))", regex_group_count=2)
56 | def parse_word_and_covert_to_uppercase(text):
57 | return text.strip().upper()
58 |
59 | @parse.with_pattern(r"\d+")
60 | def parse_number(text):
61 | return int(text)
62 |
63 | # -- CASE: Use named (OK)
64 | type_map = {"Name": parse_word_and_covert_to_uppercase, "Number": parse_number}
65 | r = parse.parse(
66 | "Hello {name:Name} {number:Number}", "Hello Alice 42", extra_types=type_map
67 | )
68 | assert r.named == {"name": "ALICE", "number": 42}
69 |
70 | # -- CASE: Use unnamed/fixed (problematic)
71 | r = parse.parse("Hello {:Name} {:Number}", "Hello Alice 42", extra_types=type_map)
72 | assert r[0] == "ALICE"
73 | assert r[1] == 42
74 |
75 |
76 | def test_unmatched_brace_doesnt_match():
77 | r = parse.parse("{who.txt", "hello")
78 | assert r is None
79 |
80 |
81 | def test_pickling_bug_110():
82 | p = parse.compile("{a:d}")
83 | # prior to the fix, this would raise an AttributeError
84 | pickle.dumps(p)
85 |
86 |
87 | def test_unused_centered_alignment_bug():
88 | r = parse.parse("{:^2S}", "foo")
89 | assert r[0] == "foo"
90 | r = parse.search("{:^2S}", "foo")
91 | assert r[0] == "foo"
92 |
93 | # specifically test for the case in issue #118 as well
94 | r = parse.parse("Column {:d}:{:^}", "Column 1: Timestep")
95 | assert r[0] == 1
96 | assert r[1] == "Timestep"
97 |
98 |
99 | def test_unused_left_alignment_bug():
100 | r = parse.parse("{:<2S}", "foo")
101 | assert r[0] == "foo"
102 | r = parse.search("{:<2S}", "foo")
103 | assert r[0] == "foo"
104 |
105 |
106 | def test_match_trailing_newline():
107 | r = parse.parse("{}", "test\n")
108 | assert r[0] == "test\n"
109 |
--------------------------------------------------------------------------------
/tests/parse_tests_with_parse_type/test_findall.py:
--------------------------------------------------------------------------------
1 | # -- REPLACE: parse with parse_type.parse
2 | from __future__ import absolute_import, print_function
3 | from parse_type import parse
4 |
5 | # -- ORIGINAL_SOURCE_STARTS_HERE:
6 | # DISABLED: import parse
7 |
8 | def test_findall():
9 | s = "".join(
10 | r.fixed[0] for r in parse.findall(">{}<", "some bold text
")
11 | )
12 | assert s == "some bold text"
13 |
14 |
15 | def test_no_evaluate_result():
16 | s = "".join(
17 | m.evaluate_result().fixed[0]
18 | for m in parse.findall(
19 | ">{}<", "some bold text
", evaluate_result=False
20 | )
21 | )
22 | assert s == "some bold text"
23 |
24 |
25 | def test_case_sensitivity():
26 | l = [r.fixed[0] for r in parse.findall("x({})x", "X(hi)X")]
27 | assert l == ["hi"]
28 |
29 | l = [r.fixed[0] for r in parse.findall("x({})x", "X(hi)X", case_sensitive=True)]
30 | assert l == []
31 |
--------------------------------------------------------------------------------
/tests/parse_tests_with_parse_type/test_parsetype.py:
--------------------------------------------------------------------------------
1 | # -- REPLACE: parse with parse_type.parse
2 | from __future__ import absolute_import, print_function
3 | from parse_type import parse
4 |
5 | # -- ORIGINAL_SOURCE_STARTS_HERE:
6 | from decimal import Decimal
7 |
8 | import pytest
9 |
10 | # DISABLED: import parse
11 |
12 |
13 | def assert_match(parser, text, param_name, expected):
14 | result = parser.parse(text)
15 | assert result[param_name] == expected
16 |
17 |
18 | def assert_mismatch(parser, text, param_name):
19 | result = parser.parse(text)
20 | assert result is None
21 |
22 |
23 | def assert_fixed_match(parser, text, expected):
24 | result = parser.parse(text)
25 | assert result.fixed == expected
26 |
27 |
28 | def assert_fixed_mismatch(parser, text):
29 | result = parser.parse(text)
30 | assert result is None
31 |
32 |
33 | def test_pattern_should_be_used():
34 | def parse_number(text):
35 | return int(text)
36 |
37 | parse_number.pattern = r"\d+"
38 | parse_number.name = "Number" # For testing only.
39 |
40 | extra_types = {parse_number.name: parse_number}
41 | format = "Value is {number:Number} and..."
42 | parser = parse.Parser(format, extra_types)
43 |
44 | assert_match(parser, "Value is 42 and...", "number", 42)
45 | assert_match(parser, "Value is 00123 and...", "number", 123)
46 | assert_mismatch(parser, "Value is ALICE and...", "number")
47 | assert_mismatch(parser, "Value is -123 and...", "number")
48 |
49 |
50 | def test_pattern_should_be_used2():
51 | def parse_yesno(text):
52 | return parse_yesno.mapping[text.lower()]
53 |
54 | parse_yesno.mapping = {
55 | "yes": True,
56 | "no": False,
57 | "on": True,
58 | "off": False,
59 | "true": True,
60 | "false": False,
61 | }
62 | parse_yesno.pattern = r"|".join(parse_yesno.mapping.keys())
63 | parse_yesno.name = "YesNo" # For testing only.
64 |
65 | extra_types = {parse_yesno.name: parse_yesno}
66 | format = "Answer: {answer:YesNo}"
67 | parser = parse.Parser(format, extra_types)
68 |
69 | # -- ENSURE: Known enum values are correctly extracted.
70 | for value_name, value in parse_yesno.mapping.items():
71 | text = "Answer: %s" % value_name
72 | assert_match(parser, text, "answer", value)
73 |
74 | # -- IGNORE-CASE: In parsing, calls type converter function !!!
75 | assert_match(parser, "Answer: YES", "answer", True)
76 | assert_mismatch(parser, "Answer: __YES__", "answer")
77 |
78 |
79 | def test_with_pattern():
80 | ab_vals = {"a": 1, "b": 2}
81 |
82 | @parse.with_pattern(r"[ab]")
83 | def ab(text):
84 | return ab_vals[text]
85 |
86 | parser = parse.Parser("test {result:ab}", {"ab": ab})
87 | assert_match(parser, "test a", "result", 1)
88 | assert_match(parser, "test b", "result", 2)
89 | assert_mismatch(parser, "test c", "result")
90 |
91 |
92 | def test_with_pattern_and_regex_group_count():
93 | # -- SPECIAL-CASE: Regex-grouping is used in user-defined type
94 | # NOTE: Missing or wroung regex_group_counts cause problems
95 | # with parsing following params.
96 | @parse.with_pattern(r"(meter|kilometer)", regex_group_count=1)
97 | def parse_unit(text):
98 | return text.strip()
99 |
100 | @parse.with_pattern(r"\d+")
101 | def parse_number(text):
102 | return int(text)
103 |
104 | type_converters = {"Number": parse_number, "Unit": parse_unit}
105 | # -- CASE: Unnamed-params (affected)
106 | parser = parse.Parser("test {:Unit}-{:Number}", type_converters)
107 | assert_fixed_match(parser, "test meter-10", ("meter", 10))
108 | assert_fixed_match(parser, "test kilometer-20", ("kilometer", 20))
109 | assert_fixed_mismatch(parser, "test liter-30")
110 |
111 | # -- CASE: Named-params (uncritical; should not be affected)
112 | # REASON: Named-params have additional, own grouping.
113 | parser2 = parse.Parser("test {unit:Unit}-{value:Number}", type_converters)
114 | assert_match(parser2, "test meter-10", "unit", "meter")
115 | assert_match(parser2, "test meter-10", "value", 10)
116 | assert_match(parser2, "test kilometer-20", "unit", "kilometer")
117 | assert_match(parser2, "test kilometer-20", "value", 20)
118 | assert_mismatch(parser2, "test liter-30", "unit")
119 |
120 |
121 | def test_with_pattern_and_wrong_regex_group_count_raises_error():
122 | # -- SPECIAL-CASE:
123 | # Regex-grouping is used in user-defined type, but wrong value is provided.
124 | @parse.with_pattern(r"(meter|kilometer)", regex_group_count=1)
125 | def parse_unit(text):
126 | return text.strip()
127 |
128 | @parse.with_pattern(r"\d+")
129 | def parse_number(text):
130 | return int(text)
131 |
132 | # -- CASE: Unnamed-params (affected)
133 | BAD_REGEX_GROUP_COUNTS_AND_ERRORS = [
134 | (None, ValueError),
135 | (0, ValueError),
136 | (2, IndexError),
137 | ]
138 | for bad_regex_group_count, error_class in BAD_REGEX_GROUP_COUNTS_AND_ERRORS:
139 | parse_unit.regex_group_count = bad_regex_group_count # -- OVERRIDE-HERE
140 | type_converters = {"Number": parse_number, "Unit": parse_unit}
141 | parser = parse.Parser("test {:Unit}-{:Number}", type_converters)
142 | with pytest.raises(error_class):
143 | parser.parse("test meter-10")
144 |
145 |
146 | def test_with_pattern_and_regex_group_count_is_none():
147 | # -- CORNER-CASE: Increase code-coverage.
148 | data_values = {"a": 1, "b": 2}
149 |
150 | @parse.with_pattern(r"[ab]")
151 | def parse_data(text):
152 | return data_values[text]
153 |
154 | parse_data.regex_group_count = None # ENFORCE: None
155 |
156 | # -- CASE: Unnamed-params
157 | parser = parse.Parser("test {:Data}", {"Data": parse_data})
158 | assert_fixed_match(parser, "test a", (1,))
159 | assert_fixed_match(parser, "test b", (2,))
160 | assert_fixed_mismatch(parser, "test c")
161 |
162 | # -- CASE: Named-params
163 | parser2 = parse.Parser("test {value:Data}", {"Data": parse_data})
164 | assert_match(parser2, "test a", "value", 1)
165 | assert_match(parser2, "test b", "value", 2)
166 | assert_mismatch(parser2, "test c", "value")
167 |
168 |
169 | def test_case_sensitivity():
170 | r = parse.parse("SPAM {} SPAM", "spam spam spam")
171 | assert r[0] == "spam"
172 | assert parse.parse("SPAM {} SPAM", "spam spam spam", case_sensitive=True) is None
173 |
174 |
175 | def test_decimal_value():
176 | value = Decimal("5.5")
177 | str_ = "test {}".format(value)
178 | parser = parse.Parser("test {:F}")
179 | assert parser.parse(str_)[0] == value
180 |
181 |
182 | def test_width_str():
183 | res = parse.parse("{:.2}{:.2}", "look")
184 | assert res.fixed == ("lo", "ok")
185 | res = parse.parse("{:2}{:2}", "look")
186 | assert res.fixed == ("lo", "ok")
187 | res = parse.parse("{:4}{}", "look at that")
188 | assert res.fixed == ("look", " at that")
189 |
190 |
191 | def test_width_constraints():
192 | res = parse.parse("{:4}", "looky")
193 | assert res.fixed == ("looky",)
194 | res = parse.parse("{:4.4}", "looky")
195 | assert res is None
196 | res = parse.parse("{:4.4}", "ook")
197 | assert res is None
198 | res = parse.parse("{:4}{:.4}", "look at that")
199 | assert res.fixed == ("look at ", "that")
200 |
201 |
202 | def test_width_multi_int():
203 | res = parse.parse("{:02d}{:02d}", "0440")
204 | assert res.fixed == (4, 40)
205 | res = parse.parse("{:03d}{:d}", "04404")
206 | assert res.fixed == (44, 4)
207 |
208 |
209 | def test_width_empty_input():
210 | res = parse.parse("{:.2}", "")
211 | assert res is None
212 | res = parse.parse("{:2}", "l")
213 | assert res is None
214 | res = parse.parse("{:2d}", "")
215 | assert res is None
216 |
217 |
218 | def test_int_convert_stateless_base():
219 | parser = parse.Parser("{:d}")
220 | assert parser.parse("1234")[0] == 1234
221 | assert parser.parse("0b1011")[0] == 0b1011
222 |
--------------------------------------------------------------------------------
/tests/parse_tests_with_parse_type/test_pattern.py:
--------------------------------------------------------------------------------
1 | # -- REPLACE: parse with parse_type.parse
2 | from __future__ import absolute_import, print_function
3 | from parse_type import parse
4 |
5 | # -- ORIGINAL_SOURCE_STARTS_HERE:
6 | import pytest
7 |
8 | # DISABLED: import parse
9 |
10 |
11 | def _test_expression(format, expression):
12 | assert parse.Parser(format)._expression == expression
13 |
14 |
15 | def test_braces():
16 | # pull a simple string out of another string
17 | _test_expression("{{ }}", r"\{ \}")
18 |
19 |
20 | def test_fixed():
21 | # pull a simple string out of another string
22 | _test_expression("{}", r"(.+?)")
23 | _test_expression("{} {}", r"(.+?) (.+?)")
24 |
25 |
26 | def test_named():
27 | # pull a named string out of another string
28 | _test_expression("{name}", r"(?P.+?)")
29 | _test_expression("{name} {other}", r"(?P.+?) (?P.+?)")
30 |
31 |
32 | def test_named_typed():
33 | # pull a named string out of another string
34 | _test_expression("{name:w}", r"(?P\w+)")
35 | _test_expression("{name:w} {other:w}", r"(?P\w+) (?P\w+)")
36 |
37 |
38 | def test_numbered():
39 | _test_expression("{0}", r"(.+?)")
40 | _test_expression("{0} {1}", r"(.+?) (.+?)")
41 | _test_expression("{0:f} {1:f}", r"([-+ ]?\d*\.\d+) ([-+ ]?\d*\.\d+)")
42 |
43 |
44 | def test_bird():
45 | # skip some trailing whitespace
46 | _test_expression("{:>}", r" *(.+?)")
47 |
48 |
49 | def test_format_variety():
50 | def _(fmt, matches):
51 | d = parse.extract_format(fmt, {"spam": "spam"})
52 | for k in matches:
53 | assert d.get(k) == matches[k]
54 |
55 | for t in "%obxegfdDwWsS":
56 | _(t, {"type": t})
57 | _("10" + t, {"type": t, "width": "10"})
58 | _("05d", {"type": "d", "width": "5", "zero": True})
59 | _("<", {"align": "<"})
60 | _(".<", {"align": "<", "fill": "."})
61 | _(">", {"align": ">"})
62 | _(".>", {"align": ">", "fill": "."})
63 | _("^", {"align": "^"})
64 | _(".^", {"align": "^", "fill": "."})
65 | _("x=d", {"type": "d", "align": "=", "fill": "x"})
66 | _("d", {"type": "d"})
67 | _("ti", {"type": "ti"})
68 | _("spam", {"type": "spam"})
69 |
70 | _(".^010d", {"type": "d", "width": "10", "align": "^", "fill": ".", "zero": True})
71 | _(".2f", {"type": "f", "precision": "2"})
72 | _("10.2f", {"type": "f", "width": "10", "precision": "2"})
73 |
74 |
75 | def test_dot_separated_fields():
76 | # this should just work and provide the named value
77 | res = parse.parse("{hello.world}_{jojo.foo.baz}_{simple}", "a_b_c")
78 | assert res.named["hello.world"] == "a"
79 | assert res.named["jojo.foo.baz"] == "b"
80 | assert res.named["simple"] == "c"
81 |
82 |
83 | def test_dict_style_fields():
84 | res = parse.parse("{hello[world]}_{hello[foo][baz]}_{simple}", "a_b_c")
85 | assert res.named["hello"]["world"] == "a"
86 | assert res.named["hello"]["foo"]["baz"] == "b"
87 | assert res.named["simple"] == "c"
88 |
89 |
90 | def test_dot_separated_fields_name_collisions():
91 | # this should just work and provide the named value
92 | res = parse.parse("{a_.b}_{a__b}_{a._b}_{a___b}", "a_b_c_d")
93 | assert res.named["a_.b"] == "a"
94 | assert res.named["a__b"] == "b"
95 | assert res.named["a._b"] == "c"
96 | assert res.named["a___b"] == "d"
97 |
98 |
99 | def test_invalid_groupnames_are_handled_gracefully():
100 | with pytest.raises(NotImplementedError):
101 | parse.parse("{hello['world']}", "doesn't work")
102 |
--------------------------------------------------------------------------------
/tests/parse_tests_with_parse_type/test_result.py:
--------------------------------------------------------------------------------
1 | # -- REPLACE: parse with parse_type.parse
2 | from __future__ import absolute_import, print_function
3 | from parse_type import parse
4 |
5 | # -- ORIGINAL_SOURCE_STARTS_HERE:
6 | import pytest
7 |
8 | # DISABLED: import parse
9 |
10 |
11 | def test_fixed_access():
12 | r = parse.Result((1, 2), {}, None)
13 | assert r[0] == 1
14 | assert r[1] == 2
15 | with pytest.raises(IndexError):
16 | r[2]
17 | with pytest.raises(KeyError):
18 | r["spam"]
19 |
20 |
21 | def test_slice_access():
22 | r = parse.Result((1, 2, 3, 4), {}, None)
23 | assert r[1:3] == (2, 3)
24 | assert r[-5:5] == (1, 2, 3, 4)
25 | assert r[:4:2] == (1, 3)
26 | assert r[::-2] == (4, 2)
27 | assert r[5:10] == ()
28 |
29 |
30 | def test_named_access():
31 | r = parse.Result((), {"spam": "ham"}, None)
32 | assert r["spam"] == "ham"
33 | with pytest.raises(KeyError):
34 | r["ham"]
35 | with pytest.raises(IndexError):
36 | r[0]
37 |
38 |
39 | def test_contains():
40 | r = parse.Result(("cat",), {"spam": "ham"}, None)
41 | assert "spam" in r
42 | assert "cat" not in r
43 | assert "ham" not in r
44 |
--------------------------------------------------------------------------------
/tests/parse_tests_with_parse_type/test_search.py:
--------------------------------------------------------------------------------
1 | # -- REPLACE: parse with parse_type.parse
2 | from __future__ import absolute_import, print_function
3 | from parse_type import parse
4 |
5 | # -- ORIGINAL_SOURCE_STARTS_HERE:
6 | # import parse
7 |
8 | def test_basic():
9 | r = parse.search("a {} c", " a b c ")
10 | assert r.fixed == ("b",)
11 |
12 |
13 | def test_multiline():
14 | r = parse.search("age: {:d}\n", "name: Rufus\nage: 42\ncolor: red\n")
15 | assert r.fixed == (42,)
16 |
17 |
18 | def test_pos():
19 | r = parse.search("a {} c", " a b c ", 2)
20 | assert r is None
21 |
22 |
23 | def test_no_evaluate_result():
24 | match = parse.search(
25 | "age: {:d}\n", "name: Rufus\nage: 42\ncolor: red\n", evaluate_result=False
26 | )
27 | r = match.evaluate_result()
28 | assert r.fixed == (42,)
29 |
--------------------------------------------------------------------------------
/tests/parse_type_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from __future__ import absolute_import
4 | from parse_type import TypeBuilder
5 | from enum import Enum
6 | try:
7 | import unittest2 as unittest
8 | except ImportError:
9 | import unittest
10 |
11 |
12 | # -----------------------------------------------------------------------------
13 | # TEST SUPPORT FOR: TypeBuilder Tests
14 | # -----------------------------------------------------------------------------
15 | # -- PROOF-OF-CONCEPT DATATYPE:
16 | def parse_number(text):
17 | return int(text)
18 | parse_number.pattern = r"\d+" # Provide better regexp pattern than default.
19 | parse_number.name = "Number" # For testing only.
20 |
21 | # -- ENUM DATATYPE:
22 | parse_yesno = TypeBuilder.make_enum({
23 | "yes": True, "no": False,
24 | "on": True, "off": False,
25 | "true": True, "false": False,
26 | })
27 | parse_yesno.name = "YesNo" # For testing only.
28 |
29 | # -- ENUM CLASS:
30 | class Color(Enum):
31 | red = 1
32 | green = 2
33 | blue = 3
34 |
35 | parse_color = TypeBuilder.make_enum(Color)
36 | parse_color.name = "Color"
37 |
38 | # -- CHOICE DATATYPE:
39 | parse_person_choice = TypeBuilder.make_choice(["Alice", "Bob", "Charly"])
40 | parse_person_choice.name = "PersonChoice" # For testing only.
41 |
42 |
43 | # -----------------------------------------------------------------------------
44 | # ABSTRACT TEST CASE:
45 | # -----------------------------------------------------------------------------
46 | class TestCase(unittest.TestCase):
47 |
48 | # -- PYTHON VERSION BACKWARD-COMPATIBILTY:
49 | if not hasattr(unittest.TestCase, "assertIsNone"):
50 | def assertIsNone(self, obj, msg=None):
51 | self.assert_(obj is None, msg)
52 |
53 | def assertIsNotNone(self, obj, msg=None):
54 | self.assert_(obj is not None, msg)
55 |
56 |
57 | class ParseTypeTestCase(TestCase):
58 | """
59 | Common test case base class for :mod:`parse_type` tests.
60 | """
61 |
62 | def assert_match(self, parser, text, param_name, expected):
63 | """
64 | Check that a parser can parse the provided text and extracts the
65 | expected value for a parameter.
66 |
67 | :param parser: Parser to use
68 | :param text: Text to parse
69 | :param param_name: Name of parameter
70 | :param expected: Expected value of parameter.
71 | :raise: AssertionError on failures.
72 | """
73 | result = parser.parse(text)
74 | self.assertIsNotNone(result)
75 | self.assertEqual(result[param_name], expected)
76 |
77 | def assert_mismatch(self, parser, text, param_name=None):
78 | """
79 | Check that a parser cannot extract the parameter from the provided text.
80 | A parse mismatch has occured.
81 |
82 | :param parser: Parser to use
83 | :param text: Text to parse
84 | :param param_name: Name of parameter
85 | :raise: AssertionError on failures.
86 | """
87 | result = parser.parse(text)
88 | self.assertIsNone(result)
89 |
90 | def ensure_can_parse_all_enum_values(self, parser, type_converter,
91 | schema, name):
92 | # -- ENSURE: Known enum values are correctly extracted.
93 | for value_name, value in type_converter.mappings.items():
94 | text = schema % value_name
95 | self.assert_match(parser, text, name, value)
96 |
97 | def ensure_can_parse_all_choices(self, parser, type_converter, schema, name):
98 | transform = getattr(type_converter, "transform", None)
99 | for choice_value in type_converter.choices:
100 | text = schema % choice_value
101 | expected_value = choice_value
102 | if transform:
103 | assert callable(transform)
104 | expected_value = transform(choice_value)
105 | self.assert_match(parser, text, name, expected_value)
106 |
107 | def ensure_can_parse_all_choices2(self, parser, type_converter, schema, name):
108 | transform = getattr(type_converter, "transform", None)
109 | for index, choice_value in enumerate(type_converter.choices):
110 | text = schema % choice_value
111 | if transform:
112 | assert callable(transform)
113 | expected_value = (index, transform(choice_value))
114 | else:
115 | expected_value = (index, choice_value)
116 | self.assert_match(parser, text, name, expected_value)
117 |
118 |
119 |
120 | # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type)
121 | #
122 | # Permission is hereby granted, free of charge, to any person obtaining a copy
123 | # of this software and associated documentation files (the "Software"), to deal
124 | # in the Software without restriction, including without limitation the rights
125 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
126 | # copies of the Software, and to permit persons to whom the Software is
127 | # furnished to do so, subject to the following conditions:
128 | #
129 | # The above copyright notice and this permission notice shall be included in
130 | # all copies or substantial portions of the Software.
131 | #
132 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
133 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
134 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
135 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
136 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
137 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
138 | # SOFTWARE.
139 |
--------------------------------------------------------------------------------
/tests/test_cardinality_field0.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | Test experiment for parse.
5 | Add cardinality format field after type:
6 |
7 | "... {person:Person?} ..." -- CARDINALITY: Zero or one, 0..1 (optional)
8 | "... {persons:Person*} ..." -- CARDINALITY: Zero or more, 0..N (many0)
9 | "... {persons:Person+} ..." -- CARDINALITY: One or more, 1..N (many)
10 |
11 |
12 | REQUIRES:
13 | parse >= 1.5.3.1 ('pattern' attribute support and further extensions)
14 |
15 | STATUS:
16 | IDEA, working prototype with patched parse module, but not accepted.
17 | """
18 |
19 | from __future__ import absolute_import
20 | from .parse_type_test import ParseTypeTestCase
21 | from parse_type import TypeBuilder, build_type_dict
22 | import parse
23 | import unittest
24 |
25 | ENABLED = False
26 | if ENABLED:
27 | # -------------------------------------------------------------------------
28 | # TEST CASE: TestParseTypeWithCardinalityField
29 | # -------------------------------------------------------------------------
30 | class TestParseTypeWithCardinalityField(ParseTypeTestCase):
31 | """
32 | Test cardinality field part in parse type expressions, ala:
33 |
34 | "... {person:Person?} ..." -- OPTIONAL: cardinality is zero or one.
35 | "... {persons:Person*} ..." -- MANY0: cardinality is zero or more.
36 | "... {persons:Person+} ..." -- MANY: cardinality is one or more.
37 |
38 | NOTE:
39 | * TypeBuilder has a similar and slightly more flexible feature.
40 | * Cardinality field part works currently only for user-defined types.
41 | """
42 |
43 | def test_without_cardinality_field(self):
44 | # -- IMPLCIT CARDINALITY: one
45 | # -- SETUP:
46 | parse_person = TypeBuilder.make_choice(["Alice", "Bob", "Charly"])
47 | parse_person.name = "Person" # For testing only.
48 | extra_types = build_type_dict([ parse_person ])
49 | schema = "One: {person:Person}"
50 | parser = parse.Parser(schema, extra_types)
51 |
52 | # -- PERFORM TESTS:
53 | self.assert_match(parser, "One: Alice", "person", "Alice")
54 | self.assert_match(parser, "One: Bob", "person", "Bob")
55 |
56 | # -- PARSE MISMATCH:
57 | self.assert_mismatch(parser, "One: ", "person") # Missing.
58 | self.assert_mismatch(parser, "One: BAlice", "person") # Similar1.
59 | self.assert_mismatch(parser, "One: Boby", "person") # Similar2.
60 | self.assert_mismatch(parser, "One: a", "person") # INVALID ...
61 |
62 | def test_cardinality_field_with_zero_or_one(self):
63 | # -- SETUP:
64 | parse_person = TypeBuilder.make_choice(["Alice", "Bob", "Charly"])
65 | parse_person.name = "Person" # For testing only.
66 | extra_types = build_type_dict([ parse_person ])
67 | schema = "Optional: {person:Person?}"
68 | parser = parse.Parser(schema, extra_types)
69 |
70 | # -- PERFORM TESTS:
71 | self.assert_match(parser, "Optional: ", "person", None)
72 | self.assert_match(parser, "Optional: Alice", "person", "Alice")
73 | self.assert_match(parser, "Optional: Bob", "person", "Bob")
74 |
75 | # -- PARSE MISMATCH:
76 | self.assert_mismatch(parser, "Optional: Anna", "person") # Similar1.
77 | self.assert_mismatch(parser, "Optional: Boby", "person") # Similar2.
78 | self.assert_mismatch(parser, "Optional: a", "person") # INVALID ...
79 |
80 | def test_cardinality_field_with_one_or_more(self):
81 | # -- SETUP:
82 | parse_person = TypeBuilder.make_choice(["Alice", "Bob", "Charly"])
83 | parse_person.name = "Person" # For testing only.
84 | extra_types = build_type_dict([ parse_person ])
85 | schema = "List: {persons:Person+}"
86 | parser = parse.Parser(schema, extra_types)
87 |
88 | # -- PERFORM TESTS:
89 | self.assert_match(parser, "List: Alice", "persons", [ "Alice" ])
90 | self.assert_match(parser, "List: Bob", "persons", [ "Bob" ])
91 | self.assert_match(parser, "List: Bob, Alice",
92 | "persons", [ "Bob", "Alice" ])
93 |
94 | # -- PARSE MISMATCH:
95 | self.assert_mismatch(parser, "List: ", "persons") # Zero items.
96 | self.assert_mismatch(parser, "List: BAlice", "persons") # Unknown1.
97 | self.assert_mismatch(parser, "List: Boby", "persons") # Unknown2.
98 | self.assert_mismatch(parser, "List: Alice,", "persons") # Trailing,
99 | self.assert_mismatch(parser, "List: a, b", "persons") # List of...
100 |
101 | def test_cardinality_field_with_zero_or_more(self):
102 | # -- SETUP:
103 | parse_person = TypeBuilder.make_choice(["Alice", "Bob", "Charly"])
104 | parse_person.name = "Person" # For testing only.
105 | extra_types = build_type_dict([ parse_person ])
106 | schema = "List: {persons:Person*}"
107 | parser = parse.Parser(schema, extra_types)
108 |
109 | # -- PERFORM TESTS:
110 | self.assert_match(parser, "List: ", "persons", [ ])
111 | self.assert_match(parser, "List: Alice", "persons", [ "Alice" ])
112 | self.assert_match(parser, "List: Bob", "persons", [ "Bob" ])
113 | self.assert_match(parser, "List: Bob, Alice",
114 | "persons", [ "Bob", "Alice" ])
115 |
116 | # -- PARSE MISMATCH:
117 | self.assert_mismatch(parser, "List:", "persons") # Too short.
118 | self.assert_mismatch(parser, "List: BAlice", "persons") # Unknown1.
119 | self.assert_mismatch(parser, "List: Boby", "persons") # Unknown2.
120 | self.assert_mismatch(parser, "List: Alice,", "persons") # Trailing,
121 | self.assert_mismatch(parser, "List: a, b", "persons") # List of...
122 |
123 | # -----------------------------------------------------------------------------
124 | # MAIN:
125 | # -----------------------------------------------------------------------------
126 | if __name__ == '__main__':
127 | unittest.main()
128 |
129 |
130 | # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type)
131 | #
132 | # Permission is hereby granted, free of charge, to any person obtaining a copy
133 | # of this software and associated documentation files (the "Software"), to deal
134 | # in the Software without restriction, including without limitation the rights
135 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
136 | # copies of the Software, and to permit persons to whom the Software is
137 | # furnished to do so, subject to the following conditions:
138 | #
139 | # The above copyright notice and this permission notice shall be included in
140 | # all copies or substantial portions of the Software.
141 | #
142 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
143 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
144 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
145 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
146 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
147 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
148 | # SOFTWARE.
149 |
--------------------------------------------------------------------------------
/tests/test_cfparse.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #!/usr/bin/env python
3 | # -*- coding: utf-8 -*-
4 | """
5 | Test suite to test the :mod:`parse_type.cfparse` module.
6 | """
7 |
8 | from __future__ import absolute_import
9 | from .parse_type_test import ParseTypeTestCase, parse_number, unittest
10 | from parse_type.cfparse import Parser
11 | from parse_type.cardinality_field \
12 | import MissingTypeError, CardinalityFieldTypeBuilder
13 |
14 |
15 | # -----------------------------------------------------------------------------
16 | # TEST CASE:
17 | # -----------------------------------------------------------------------------
18 | class TestParser(ParseTypeTestCase):
19 | """
20 | Test :class:`parse_type.cfparse.Parser`.
21 | Ensure that:
22 |
23 | * parser can parse fields with CardinalityField part
24 | even when these special type variants are not provided.
25 |
26 | * parser creates missing type converter variants for CardinalityFields
27 | as long as the primary type converter for cardinality=1 is provided.
28 | """
29 | SPECIAL_FIELD_TYPES_DATA = [
30 | ("{number1:Number?}", ["Number?"]),
31 | ("{number2:Number+}", ["Number+"]),
32 | ("{number3:Number*}", ["Number*"]),
33 | ("{number1:Number?} {number2:Number+} {number3:Number*}",
34 | ["Number?", "Number+", "Number*"]),
35 | ]
36 |
37 | def test_parser__can_parse_normal_fields(self):
38 | existing_types = dict(Number=parse_number)
39 | schema = "Number: {number:Number}"
40 | parser = Parser(schema, existing_types)
41 | self.assert_match(parser, "Number: 42", "number", 42)
42 | self.assert_match(parser, "Number: 123", "number", 123)
43 | self.assert_mismatch(parser, "Number: ")
44 | self.assert_mismatch(parser, "Number: XXX")
45 | self.assert_mismatch(parser, "Number: -123")
46 |
47 | def test_parser__can_parse_cardinality_field_optional(self):
48 | # -- CARDINALITY: 0..1 = zero_or_one = optional
49 | existing_types = dict(Number=parse_number)
50 | self.assertFalse("Number?" in existing_types)
51 |
52 | # -- ENSURE: Missing type variant is created.
53 | schema = "OptionalNumber: {number:Number?}"
54 | parser = Parser(schema, existing_types)
55 | self.assertTrue("Number?" in existing_types)
56 |
57 | # -- ENSURE: Newly created type variant is usable.
58 | self.assert_match(parser, "OptionalNumber: 42", "number", 42)
59 | self.assert_match(parser, "OptionalNumber: 123", "number", 123)
60 | self.assert_match(parser, "OptionalNumber: ", "number", None)
61 | self.assert_mismatch(parser, "OptionalNumber:")
62 | self.assert_mismatch(parser, "OptionalNumber: XXX")
63 | self.assert_mismatch(parser, "OptionalNumber: -123")
64 |
65 | def test_parser__can_parse_cardinality_field_many(self):
66 | # -- CARDINALITY: 1..* = one_or_more = many
67 | existing_types = dict(Number=parse_number)
68 | self.assertFalse("Number+" in existing_types)
69 |
70 | # -- ENSURE: Missing type variant is created.
71 | schema = "List: {numbers:Number+}"
72 | parser = Parser(schema, existing_types)
73 | self.assertTrue("Number+" in existing_types)
74 |
75 | # -- ENSURE: Newly created type variant is usable.
76 | self.assert_match(parser, "List: 42", "numbers", [42])
77 | self.assert_match(parser, "List: 1, 2, 3", "numbers", [1, 2, 3])
78 | self.assert_match(parser, "List: 4,5,6", "numbers", [4, 5, 6])
79 | self.assert_mismatch(parser, "List: ")
80 | self.assert_mismatch(parser, "List:")
81 | self.assert_mismatch(parser, "List: XXX")
82 | self.assert_mismatch(parser, "List: -123")
83 |
84 | def test_parser__can_parse_cardinality_field_many_with_own_type_builder(self):
85 | # -- CARDINALITY: 1..* = one_or_more = many
86 | class MyCardinalityFieldTypeBuilder(CardinalityFieldTypeBuilder):
87 | listsep = ';'
88 |
89 | type_builder = MyCardinalityFieldTypeBuilder
90 | existing_types = dict(Number=parse_number)
91 | self.assertFalse("Number+" in existing_types)
92 |
93 | # -- ENSURE: Missing type variant is created.
94 | schema = "List: {numbers:Number+}"
95 | parser = Parser(schema, existing_types, type_builder=type_builder)
96 | self.assertTrue("Number+" in existing_types)
97 |
98 | # -- ENSURE: Newly created type variant is usable.
99 | # NOTE: Use other list separator.
100 | self.assert_match(parser, "List: 42", "numbers", [42])
101 | self.assert_match(parser, "List: 1; 2; 3", "numbers", [1, 2, 3])
102 | self.assert_match(parser, "List: 4;5;6", "numbers", [4, 5, 6])
103 | self.assert_mismatch(parser, "List: ")
104 | self.assert_mismatch(parser, "List:")
105 | self.assert_mismatch(parser, "List: XXX")
106 | self.assert_mismatch(parser, "List: -123")
107 |
108 | def test_parser__can_parse_cardinality_field_many0(self):
109 | # -- CARDINALITY: 0..* = zero_or_more = many0
110 | existing_types = dict(Number=parse_number)
111 | self.assertFalse("Number*" in existing_types)
112 |
113 | # -- ENSURE: Missing type variant is created.
114 | schema = "List0: {numbers:Number*}"
115 | parser = Parser(schema, existing_types)
116 | self.assertTrue("Number*" in existing_types)
117 |
118 | # -- ENSURE: Newly created type variant is usable.
119 | self.assert_match(parser, "List0: 42", "numbers", [42])
120 | self.assert_match(parser, "List0: 1, 2, 3", "numbers", [1, 2, 3])
121 | self.assert_match(parser, "List0: ", "numbers", [])
122 | self.assert_mismatch(parser, "List0:")
123 | self.assert_mismatch(parser, "List0: XXX")
124 | self.assert_mismatch(parser, "List0: -123")
125 |
126 |
127 | def test_create_missing_types__without_cardinality_fields_in_schema(self):
128 | schemas = ["{}", "{:Number}", "{number3}", "{number4:Number}", "XXX"]
129 | existing_types = {}
130 | for schema in schemas:
131 | new_types = Parser.create_missing_types(schema, existing_types)
132 | self.assertEqual(len(new_types), 0)
133 | self.assertEqual(new_types, {})
134 |
135 | def test_create_missing_types__raises_error_if_primary_type_is_missing(self):
136 | # -- HINT: primary type is not provided in type_dict (existing_types)
137 | existing_types = {}
138 | for schema, missing_types in self.SPECIAL_FIELD_TYPES_DATA:
139 | with self.assertRaises(MissingTypeError):
140 | Parser.create_missing_types(schema, existing_types)
141 |
142 | def test_create_missing_types__if_special_types_are_missing(self):
143 | existing_types = dict(Number=parse_number)
144 | for schema, missing_types in self.SPECIAL_FIELD_TYPES_DATA:
145 | new_types = Parser.create_missing_types(schema, existing_types)
146 | self.assertSequenceEqual(set(new_types.keys()), set(missing_types))
147 |
148 | def test_create_missing_types__if_special_types_exist(self):
149 | existing_types = dict(Number=parse_number)
150 | for schema, missing_types in self.SPECIAL_FIELD_TYPES_DATA:
151 | # -- FIRST STEP: Prepare
152 | new_types = Parser.create_missing_types(schema, existing_types)
153 | self.assertGreater(len(new_types), 0)
154 |
155 | # -- SECOND STEP: Now all needed special types should exist.
156 | existing_types2 = existing_types.copy()
157 | existing_types2.update(new_types)
158 | new_types2 = Parser.create_missing_types(schema, existing_types2)
159 | self.assertEqual(len(new_types2), 0)
160 |
161 |
162 | # -----------------------------------------------------------------------------
163 | # MAIN:
164 | # -----------------------------------------------------------------------------
165 | if __name__ == '__main__':
166 | unittest.main()
167 |
168 |
169 | # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type)
170 | #
171 | # Permission is hereby granted, free of charge, to any person obtaining a copy
172 | # of this software and associated documentation files (the "Software"), to deal
173 | # in the Software without restriction, including without limitation the rights
174 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
175 | # copies of the Software, and to permit persons to whom the Software is
176 | # furnished to do so, subject to the following conditions:
177 | #
178 | # The above copyright notice and this permission notice shall be included in
179 | # all copies or substantial portions of the Software.
180 | #
181 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
182 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
183 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
184 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
185 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
186 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
187 | # SOFTWARE.
188 |
--------------------------------------------------------------------------------
/tests/test_parse_decorator.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # pylint: disable=invalid-name, missing-docstring, too-few-public-methods
4 | """
5 | Integrated into :mod:`parse` module.
6 | """
7 |
8 | from __future__ import absolute_import
9 | import unittest
10 | import parse
11 | from parse_type import build_type_dict
12 | from .parse_type_test import ParseTypeTestCase
13 |
14 |
15 | # -----------------------------------------------------------------------------
16 | # TEST CASE: TestParseTypeWithPatternDecorator
17 | # -----------------------------------------------------------------------------
18 | class TestParseTypeWithPatternDecorator(ParseTypeTestCase):
19 | r"""
20 | Test the pattern decorator for type-converter (parse_type) functions.
21 |
22 | >>> def parse_number(text):
23 | ... return int(text)
24 | >>> parse_number.pattern = r"\d+"
25 |
26 | is equivalent to:
27 |
28 | >>> import parse
29 | >>> @parse.with_pattern(r"\d+")
30 | ... def parse_number(text):
31 | ... return int(text)
32 |
33 | >>> assert hasattr(parse_number, "pattern")
34 | >>> assert parse_number.pattern == r"\d+"
35 | """
36 |
37 | def assert_decorated_with_pattern(self, func, expected_pattern):
38 | self.assertTrue(callable(func))
39 | self.assertTrue(hasattr(func, "pattern"))
40 | self.assertEqual(func.pattern, expected_pattern)
41 |
42 | def assert_converter_call(self, func, text, expected_value):
43 | value = func(text)
44 | self.assertEqual(value, expected_value)
45 |
46 | # -- TESTS:
47 | def test_function_with_pattern_decorator(self):
48 | @parse.with_pattern(r"\d+")
49 | def parse_number(text):
50 | return int(text)
51 |
52 | self.assert_decorated_with_pattern(parse_number, r"\d+")
53 | self.assert_converter_call(parse_number, "123", 123)
54 |
55 | def test_classmethod_with_pattern_decorator(self):
56 | choice_pattern = r"Alice|Bob|Charly"
57 | class C(object):
58 | @classmethod
59 | @parse.with_pattern(choice_pattern)
60 | def parse_choice(cls, text):
61 | return text
62 |
63 | self.assert_decorated_with_pattern(C.parse_choice, choice_pattern)
64 | self.assert_converter_call(C.parse_choice, "Alice", "Alice")
65 |
66 | def test_staticmethod_with_pattern_decorator(self):
67 | choice_pattern = r"Alice|Bob|Charly"
68 | class S(object):
69 | @staticmethod
70 | @parse.with_pattern(choice_pattern)
71 | def parse_choice(text):
72 | return text
73 |
74 | self.assert_decorated_with_pattern(S.parse_choice, choice_pattern)
75 | self.assert_converter_call(S.parse_choice, "Bob", "Bob")
76 |
77 | def test_decorated_function_with_parser(self):
78 | # -- SETUP:
79 | @parse.with_pattern(r"\d+")
80 | def parse_number(text):
81 | return int(text)
82 |
83 | parse_number.name = "Number" #< For test automation.
84 | more_types = build_type_dict([parse_number])
85 | schema = "Test: {number:Number}"
86 | parser = parse.Parser(schema, more_types)
87 |
88 | # -- PERFORM TESTS:
89 | # pylint: disable=bad-whitespace
90 | self.assert_match(parser, "Test: 1", "number", 1)
91 | self.assert_match(parser, "Test: 42", "number", 42)
92 | self.assert_match(parser, "Test: 123", "number", 123)
93 |
94 | # -- PARSE MISMATCH:
95 | self.assert_mismatch(parser, "Test: x", "number") # Not a Number.
96 | self.assert_mismatch(parser, "Test: -1", "number") # Negative.
97 | self.assert_mismatch(parser, "Test: a, b", "number") # List of ...
98 |
99 | def test_decorated_classmethod_with_parser(self):
100 | # -- SETUP:
101 | class C(object):
102 | @classmethod
103 | @parse.with_pattern(r"Alice|Bob|Charly")
104 | def parse_person(cls, text):
105 | return text
106 |
107 | more_types = {"Person": C.parse_person}
108 | schema = "Test: {person:Person}"
109 | parser = parse.Parser(schema, more_types)
110 |
111 | # -- PERFORM TESTS:
112 | # pylint: disable=bad-whitespace
113 | self.assert_match(parser, "Test: Alice", "person", "Alice")
114 | self.assert_match(parser, "Test: Bob", "person", "Bob")
115 |
116 | # -- PARSE MISMATCH:
117 | self.assert_mismatch(parser, "Test: ", "person") # Missing.
118 | self.assert_mismatch(parser, "Test: BAlice", "person") # Similar1.
119 | self.assert_mismatch(parser, "Test: Boby", "person") # Similar2.
120 | self.assert_mismatch(parser, "Test: a", "person") # INVALID ...
121 |
122 | # -----------------------------------------------------------------------------
123 | # MAIN:
124 | # -----------------------------------------------------------------------------
125 | if __name__ == '__main__':
126 | unittest.main()
127 |
128 |
129 | # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type)
130 | #
131 | # Permission is hereby granted, free of charge, to any person obtaining a copy
132 | # of this software and associated documentation files (the "Software"), to deal
133 | # in the Software without restriction, including without limitation the rights
134 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
135 | # copies of the Software, and to permit persons to whom the Software is
136 | # furnished to do so, subject to the following conditions:
137 | #
138 | # The above copyright notice and this permission notice shall be included in
139 | # all copies or substantial portions of the Software.
140 | #
141 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
142 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
143 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
144 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
145 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
146 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
147 | # SOFTWARE.
148 |
--------------------------------------------------------------------------------
/tests/test_parse_number.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | """
3 | Additional unit tests for the :mod`parse` module.
4 | Related to auto-detection of number base (base=10, 2, 8, 16).
5 | """
6 |
7 | from __future__ import absolute_import, print_function
8 | import pytest
9 | import parse
10 |
11 | parse_version = parse.__version__
12 | print("USING: parse-%s" % parse_version)
13 | if parse_version in ("1.17.0", "1.16.0"):
14 | # -- REQUIRES: parse >= 1.18.0 -- WORKAROUND HERE
15 | print("USING: parse_type.parse (INSTEAD)")
16 | from parse_type import parse
17 |
18 | def assert_parse_number_with_format_d(text, expected):
19 | parser = parse.Parser("{value:d}")
20 | result = parser.parse(text)
21 | assert result.named == dict(value=expected)
22 |
23 | @pytest.mark.parametrize("text, expected", [
24 | ("123", 123)
25 | ])
26 | def test_parse_number_with_base10(text, expected):
27 | assert_parse_number_with_format_d(text, expected)
28 |
29 | @pytest.mark.parametrize("text, expected", [
30 | ("0b0", 0),
31 | ("0b1011", 11),
32 | ])
33 | def test_parse_number_with_base2(text, expected):
34 | assert_parse_number_with_format_d(text, expected)
35 |
36 | @pytest.mark.parametrize("text, expected", [
37 | ("0o0", 0),
38 | ("0o10", 8),
39 | ("0o12", 10),
40 | ])
41 | def test_parse_number_with_base8(text, expected):
42 | assert_parse_number_with_format_d(text, expected)
43 |
44 | @pytest.mark.parametrize("text, expected", [
45 | ("0x0", 0),
46 | ("0x01", 1),
47 | ("0x12", 18),
48 | ])
49 | def test_parse_number_with_base16(text, expected):
50 | assert_parse_number_with_format_d(text, expected)
51 |
52 |
53 | @pytest.mark.parametrize("text1, expected1, text2, expected2", [
54 | ("0x12", 18, "12", 12)
55 | ])
56 | def test_parse_number_twice(text1, expected1, text2, expected2):
57 | """ENSURE: Issue #121 int_convert memory effect is fixed."""
58 | parser = parse.Parser("{:d}")
59 | result1 = parser.parse(text1)
60 | result2 = parser.parse(text2)
61 | assert result1.fixed[0] == expected1
62 | assert result2.fixed[0] == expected2
63 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # ============================================================================
2 | # TOX CONFIGURATION: parse_type
3 | # ============================================================================
4 | # DESCRIPTION:
5 | # Use tox to run tasks (tests, ...) in a clean virtual environment.
6 | # Tox is configured by default for online usage.
7 | #
8 | # Run tox, like:
9 | #
10 | # tox -e py27 # Runs tox with python 2.7
11 | # tox -e py39 # Runs tox with python 3.9
12 | # tox # Runs tox with all installed python versions.
13 | # tox --parallel # Runs tox in parallel mode w/ all envs.
14 | #
15 | # SEE ALSO:
16 | # * https://tox.readthedocs.io/en/latest/config.html
17 | # ============================================================================
18 | # -- ONLINE USAGE:
19 | # PIP_INDEX_URL = https://pypi.org/simple
20 |
21 | [tox]
22 | minversion = 3.10.0
23 | envlist = py312, py311, py310, py39, doctest, pypy3
24 | skip_missing_interpreters = True
25 | isolated_build = True
26 | # DISABLED: sitepackages = False
27 |
28 |
29 | # -----------------------------------------------------------------------------
30 | # TEST ENVIRONMENTS:
31 | # -----------------------------------------------------------------------------
32 | # install_command = pip install -U {opts} {packages}
33 | [testenv]
34 | install_command = pip install -U {opts} {packages}
35 | changedir = {toxinidir}
36 | commands =
37 | pytest {posargs:tests}
38 | deps =
39 | -r py.requirements/basic.txt
40 | -r py.requirements/testing.txt
41 | setenv =
42 | PYTHONPATH={toxinidir}
43 | TOXRUN = yes
44 | PYSETUP_BOOTSTRAP = no
45 |
46 |
47 | # -- SPECIAL CASE:
48 | # RELATED: https://github.com/pypa/virtualenv/issues/2284 -- macOS 12 Monterey related
49 | # NOTES:
50 | # * pip-install seems to need "--user" option.
51 | # * Script(s) do not seem to be installed any more (actually to $HOME/User area).
52 | [testenv:py27]
53 | # DISABLED: install_command = pip install --user -U {opts} {packages}
54 | install_command = pip install -U {opts} {packages}
55 | changedir = {toxinidir}
56 | commands=
57 | python -m pytest {posargs:tests}
58 | deps=
59 | {[testenv]deps}
60 | passenv =
61 | PYTHONPATH = {toxinidir}
62 | # MAYBE: allowlist_externals = curl
63 |
64 |
65 | # -- VIRTUAL-ENVIRONMENT SETUP PROCEDURE: For python 2.7
66 | # virtualenv -p python2.7 .venv_py27
67 | # source .venv_py27
68 | # scripts/ensurepip_python27.sh
69 | # python -m pip install -r py.requirements/basic.txt
70 | # python -m pip install -r py.requirements/testing.txt
71 |
72 | [testenv:doctest]
73 | basepython = python3
74 | commands =
75 | pytest --doctest-modules -v parse_type
76 | setenv =
77 | PYTHONPATH={toxinidir}
78 |
79 |
80 | # -----------------------------------------------------------------------------
81 | # MORE TEST ENVIRONMENTS:
82 | # -----------------------------------------------------------------------------
83 | [testenv:coverage]
84 | basepython = python3
85 | commands =
86 | pytest --cov=parse_type {posargs:tests}
87 | coverage combine
88 | coverage html
89 | coverage xml
90 | deps =
91 | {[testenv]deps}
92 | pytest-cov
93 | coverage>=4.0
94 | setenv =
95 | PYTHONPATH={toxinidir}
96 |
97 |
98 | [testenv:install]
99 | basepython = python3
100 | changedir = {envdir}
101 | commands =
102 | python ../../setup.py install -q
103 | {toxinidir}/bin/toxcmd.py copytree ../../tests .
104 | pytest {posargs:tests}
105 | deps =
106 | {[testenv]deps}
107 | setenv =
108 | PYTHONPATH={toxinidir}
109 |
110 |
111 | # -----------------------------------------------------------------------------
112 | # SELDOM USED TEST ENVIRONMENTS:
113 | # -----------------------------------------------------------------------------
114 | # -- ENSURE: README.rst is well-formed.
115 | # python setup.py --long-description | rst2html.py >output.html
116 | [testenv:check_setup]
117 | changedir = {toxinidir}
118 | commands=
119 | python setup.py --long-description > output.tmp
120 | rst2html.py output.tmp output.html
121 | deps =
122 | docutils
123 |
--------------------------------------------------------------------------------