diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85b92ca..885baea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,10 @@ jobs: run-test: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: - - "3.7" - "3.8" - "3.9" - "3.10" @@ -35,6 +35,22 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} + tests-37: + name: Python 3.7 on ubuntu-20.04 + runs-on: ubuntu-20.04 + container: + image: python:3.7 + steps: + - uses: actions/checkout@v3 + - name: Run tests + run: | + pip install -r tests/requirements.txt + python -m pytest + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + tests-27: name: Python 2.7 on ubuntu-20.04 runs-on: ubuntu-20.04 diff --git a/README.rst b/README.rst index 5da36c1..6e5712d 100644 --- a/README.rst +++ b/README.rst @@ -441,6 +441,7 @@ To run the tests locally: Changelog --------- +- 1.20.2 Template field names can now contain - character i.e. HYPHEN-MINUS, chr(0x2d) - 1.20.1 The `%f` directive accepts 1-6 digits, like strptime (thanks @bbertincourt) - 1.20.0 Added support for strptime codes (thanks @bendichter) - 1.19.1 Added support for sign specifiers in number formats (thanks @anntzer) diff --git a/parse.py b/parse.py index 422a27d..51a6953 100644 --- a/parse.py +++ b/parse.py @@ -11,7 +11,7 @@ from functools import partial -__version__ = "1.20.1" +__version__ = "1.20.2" __all__ = ["parse", "search", "findall", "with_pattern"] log = logging.getLogger(__name__) @@ -398,7 +398,7 @@ def extract_format(format, extra_types): return locals() -PARSE_RE = re.compile(r"({{|}}|{\w*(?:\.\w+|\[[^]]+])*(?::[^}]+)?})") +PARSE_RE = re.compile(r"({{|}}|{[\w-]*(?:\.[\w-]+|\[[^]]+])*(?::[^}]+)?})") class Parser(object): @@ -476,11 +476,11 @@ def _match_re(self): @property def named_fields(self): - return self._named_fields.copy() + return self._named_fields[:] @property def fixed_fields(self): - return self._fixed_fields.copy() + return self._fixed_fields[:] @property def format(self): @@ -619,7 +619,7 @@ def _generate_expression(self): def _to_group_name(self, field): # return a version of field which can be used as capture group, even # though it might contain '.' - group = field.replace(".", "_").replace("[", "_").replace("]", "_") + group = field.replace(".", "_").replace("[", "_").replace("]", "_").replace("-", "_") # make sure we don't collide ("a.b" colliding with "a_b") n = 1 @@ -629,6 +629,8 @@ def _to_group_name(self, field): group = field.replace(".", "_" * n) elif "_" in field: group = field.replace("_", "_" * n) + elif "-" in field: + group = field.replace("-", "_" * n) else: raise KeyError("duplicated group name %r" % (field,)) diff --git a/tests/test_parse.py b/tests/test_parse.py index a538c73..d2ed073 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -763,3 +763,23 @@ def test_parser_format(): assert parser.format.format("world") == "hello world" with pytest.raises(AttributeError): parser.format = "hi {}" + + +def test_hyphen_inside_field_name(): + # https://github.com/r1chardj0n3s/parse/issues/86 + # https://github.com/python-openapi/openapi-core/issues/672 + template = "/local/sub/{user-id}/duration" + assert parse.Parser(template).named_fields == ["user_id"] + string = "https://dummy_server.com/local/sub/1647222638/duration" + result = parse.search(template, string) + assert result["user-id"] == "1647222638" + + +def test_hyphen_inside_field_name_collision_handling(): + template = "/foo/{user-id}/{user_id}/{user.id}/bar/" + assert parse.Parser(template).named_fields == ["user_id", "user__id", "user___id"] + string = "/foo/1/2/3/bar/" + result = parse.search(template, string) + assert result["user-id"] == "1" + assert result["user_id"] == "2" + assert result["user.id"] == "3"