From 464f408d67522fa76ea33664bdb8d7781e27219e Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sun, 12 Nov 2023 00:58:05 -0300 Subject: [PATCH] Modernize project infrastructure 1. set 3.9 as lowest python version 2. migrated to pyproject.toml 3. migrated to pytest 4. migrated to ruff and ruff-format 5. add publish github action 6. migrated readme to markdown --- .github/pull_request_template.md | 2 +- .github/workflows/ci.yml | 3 +- .github/workflows/publish.yml | 27 ++ .pre-commit-config.yaml | 8 +- CHANGES | 2 +- MANIFEST.in | 2 +- README.md | 80 +++++ README.rst | 105 ------ pyproject.toml | 54 +++ requirements.test.txt | 1 + requirements.txt | 0 setup.cfg | 67 ---- setup.py | 5 - stringparser.py | 8 +- tests/stringparser_test.py | 583 ++++++++++++++++--------------- 15 files changed, 474 insertions(+), 473 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 README.md delete mode 100644 README.rst create mode 100644 pyproject.toml create mode 100644 requirements.test.txt create mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8ee5e75..b012d24 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,5 @@ - [ ] Closes # (insert issue number) -- [ ] Executed ``pre-commit run --all-files`` with no errors +- [ ] Executed `pre-commit run --all-files` with no errors - [ ] The change is fully covered by automated unit tests - [ ] Documented in docs/ as appropriate - [ ] Added an entry to the CHANGES file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2d3fc3..f645f83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9, "3.10", "3.11"] -# python-version: [2.7, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: [3.9, "3.10", "3.11", "3.12"] runs-on: ubuntu-latest env: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..3cf9f79 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,27 @@ +name: Build and publish to PyPI + +on: + push: + tags: + - '*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: python -m pip install build + + - name: Build package + run: python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c82f6f5..8420694 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,16 +5,12 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - - id: black-jupyter - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.240' + rev: v0.1.5 hooks: - id: ruff args: ["--fix"] + - id: ruff-format - repo: https://github.com/executablebooks/mdformat rev: 0.7.16 hooks: diff --git a/CHANGES b/CHANGES index 9cf03c7..ddd0c1c 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ 0.7 (unreleased) ---------------- -- Nothing changed yet. +- Migrated project to modern framework. 0.6 (2022-10-26) diff --git a/MANIFEST.in b/MANIFEST.in index 5c34685..8641a5e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include README.rst LICENSE CHANGES +include README.md LICENSE CHANGES include stringparser.py exclude .editorconfig bors.toml pull_request_template.md requirements_docs.txt version.py exclude .coveragerc .pre-commit-config.yaml tests/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..82d422e --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +[![Latest Version](https://img.shields.io/pypi/v/stringparser.svg)](https://pypi.python.org/pypi/stringparser) +[![image](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) +[![License](https://img.shields.io/pypi/l/stringparser.svg)](https://pypi.python.org/pypi/stringparser) +[![Python Versions](https://img.shields.io/pypi/pyversions/stringparser.svg)](https://pypi.python.org/pypi/stringparser) +[![CI](https://github.com/hgrecco/stringparser/workflows/CI/badge.svg)](https://github.com/hgrecco/stringparser/actions?query=workflow%3ACI) +[![LINTER](https://github.com/hgrecco/stringparser/workflows/Lint/badge.svg)](https://github.com/hgrecco/stringparser/actions?query=workflow%3ALint) +[![Coverage](https://coveralls.io/repos/github/hgrecco/stringparser/badge.svg?branch=master)](https://coveralls.io/github/hgrecco/stringparser?branch=master) + +# Motivation + +The `stringparser` module provides a simple way to match patterns and +extract information within strings. As patterns are given using the +familiar format string specification `3101`{.interpreted-text +role="pep"}, writing them is much easier than writing regular +expressions (albeit less powerful). + +Just install it using: + +```bash +pip install stringparser +``` + +# Examples + +You can build a reusable parser object: + +```python +>>> parser = Parser('The answer is {:d}') +>>> parser('The answer is 42') +42 +>>> parser('The answer is 54') +54 +``` + +Or directly: + +```python +>>> Parser('The answer is {:d}')('The answer is 42') +42 +``` + +You can retrieve many fields: + +```python +>>> Parser('The {:s} is {:d}')('The answer is 42') +('answer', 42) +``` + +And you can use numbered fields to order the returned tuple: + +```python +>>> Parser('The {1:s} is {0:d}')('The answer is 42') +(42, 'answer') +``` + +Or named fields to return an OrderedDict: + +```python +>>> Parser('The {a:s} is {b:d}')('The answer is 42') +OrderedDict([('a', 'answer'), ('b', 42)]) +``` + +You can ignore some fields using _ as a name: + +```python +>>> Parser('The {_:s} is {:d}')('The answer is 42') +42 +``` + +# Limitations + +- From the format string: + \[\[\[fill\]align\]\[sign\]\[#\]\[0\]\[minimumwidth\]\[.precision\]\[type\]\]{.title-ref} + only \[type\]{.title-ref}, \[sign\]{.title-ref} and \[#\]{.title-ref} are + currently implemented. This might cause trouble to match certain + notation like: + - decimal: '-4' written as '- 4' + - etc +- Lines are matched from beginning to end. {:d} will NOT return all + the numbers in the string. Use regex for that. diff --git a/README.rst b/README.rst deleted file mode 100644 index 09f85e7..0000000 --- a/README.rst +++ /dev/null @@ -1,105 +0,0 @@ -.. image:: https://img.shields.io/pypi/v/stringparser.svg - :target: https://pypi.python.org/pypi/stringparser - :alt: Latest Version - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/python/black - -.. image:: https://img.shields.io/pypi/l/stringparser.svg - :target: https://pypi.python.org/pypi/stringparser - :alt: License - -.. image:: https://img.shields.io/pypi/pyversions/stringparser.svg - :target: https://pypi.python.org/pypi/stringparser - :alt: Python Versions - -.. image:: https://github.com/hgrecco/stringparser/workflows/CI/badge.svg - :target: https://github.com/hgrecco/stringparser/actions?query=workflow%3ACI - :alt: CI - -.. image:: https://github.com/hgrecco/stringparser/workflows/Lint/badge.svg - :target: https://github.com/hgrecco/stringparser/actions?query=workflow%3ALint - :alt: LINTER - -.. image:: https://coveralls.io/repos/github/hgrecco/stringparser/badge.svg?branch=master - :target: https://coveralls.io/github/hgrecco/stringparser?branch=master - :alt: Coverage - - -Motivation ----------- - -The ``stringparser`` module provides a simple way to match patterns and extract -information within strings. As patterns are given using the familiar format -string specification :pep:`3101`, writing them is much easier than writing -regular expressions (albeit less powerful). - -Just install it using: - -.. code-block:: bash - - pip install stringparser - - -Examples --------- - -You can build a reusable parser object: - -.. code-block:: python - - >>> parser = Parser('The answer is {:d}') - >>> parser('The answer is 42') - 42 - >>> parser('The answer is 54') - 54 - -Or directly: - -.. code-block:: python - - >>> Parser('The answer is {:d}')('The answer is 42') - 42 - -You can retrieve many fields: - -.. code-block:: python - - >>> Parser('The {:s} is {:d}')('The answer is 42') - ('answer', 42) - -And you can use numbered fields to order the returned tuple: - -.. code-block:: python - - >>> Parser('The {1:s} is {0:d}')('The answer is 42') - (42, 'answer') - -Or named fields to return an OrderedDict: - -.. code-block:: python - - >>> Parser('The {a:s} is {b:d}')('The answer is 42') - OrderedDict([('a', 'answer'), ('b', 42)]) - -You can ignore some fields using _ as a name: - -.. code-block:: python - - >>> Parser('The {_:s} is {:d}')('The answer is 42') - 42 - - -Limitations ------------ - -- From the format string: - `[[fill]align][sign][#][0][minimumwidth][.precision][type]` - only `type`, `sign` and `#` are currently implemented. - This might cause trouble to match certain notation like: - - - decimal: '-4' written as '- 4' - - etc - -- Lines are matched from beginning to end. {:d} will NOT return all - the numbers in the string. Use regex for that. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4b48022 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +[project] +name = "stringparser" +authors = [ + {name="Hernan E. Grecco", email="hernan.grecco@gmail.com"} +] +license = "BSD-3-Clause" +description = "Easy to use pattern matching and information extraction" +keywords = ["string", "parsing", "PEP3101", "regex"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries", + "Topic :: Text Processing", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +requires-python = ">=3.9" +dynamic = ["dependencies", "optional-dependencies", "version"] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +py-modules = ["stringparser"] + +[tool.setuptools.dynamic] +dependencies = {file = "requirements.txt"} +optional-dependencies.test = {file = "requirements.test.txt"} + +[project.urls] +"Homepage" = "https://github.com/hgrecco/stringparser" +"Bug Tracker" = "https://github.com/hgrecco/stringparser/issues" + +[tool.setuptools_scm] + +[tool.pytest.ini_options] +addopts = "--import-mode=importlib" +pythonpath = "." + +[tool.ruff] +select = ["E", "F", "I"] +extend-include = ["*.ipynb"] diff --git a/requirements.test.txt b/requirements.test.txt new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/requirements.test.txt @@ -0,0 +1 @@ +pytest diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2263a5b..0000000 --- a/setup.cfg +++ /dev/null @@ -1,67 +0,0 @@ -[metadata] -name = stringparser -author = Hernan E. Grecco -author_email = hernan.grecco@gmail.com -license = BSD -version = 0.7.dev0 -description = Easy to use pattern matching and information extraction -long_description = file: README.rst -keywords = string, parsing, PEP3101, regex -url = http://github.com/hgrecco/stringparser -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: BSD License - Operating System :: MacOS :: MacOS X - Operating System :: Microsoft :: Windows - Operating System :: POSIX - Programming Language :: Python - Topic :: Software Development :: Libraries - Topic :: Text Processing - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - -[options] -py_modules = stringparser -zip_safe = True -python_requires = >=3.5 -setup_requires = setuptools - -[options.extras_require] -test = - pytest - pytest-cov - -[build-system] -requires = ["setuptools", "setuptools_scm", "wheel"] - -[flake8] -ignore= - # whitespace before ':' - doesn't work well with black - E203 - E402 - # line too long - let black worry about that - E501 - # do not assign a lambda expression, use a def - E731 - # line break before binary operator - W503 -exclude= - build - -[isort] -default_section=THIRDPARTY -known_first_party=pint -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 - -[zest.releaser] -create-wheel = yes diff --git a/setup.py b/setup.py deleted file mode 100644 index f4f9665..0000000 --- a/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 -from setuptools import setup - -if __name__ == "__main__": - setup() diff --git a/stringparser.py b/stringparser.py index 6061e72..938581d 100644 --- a/stringparser.py +++ b/stringparser.py @@ -36,7 +36,6 @@ else: from io import StringIO -from collections import OrderedDict from functools import partial @@ -78,7 +77,8 @@ class Dummy: # sign ::= "+" | "-" | " " # width ::= integer # precision ::= integer -# type ::= "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" | "n" | "o" | "s" | "x" | "X" | "%" +# type ::= "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" | "n" | "o" +# | "s" | "x" | "X" | "%" _FMT = re.compile( "(?P(?P[^{}])?[<>=\\^])?" "(?P[\\+\\- ])?(?P#)?" @@ -275,7 +275,6 @@ class Parser(object): """ def __init__(self, format_string, flags=0): - # List of tuples (name of the field, converter function) self._fields = [] @@ -289,7 +288,7 @@ def __init__(self, format_string, flags=0): # Assembly regex, list of fields, converter function, # and output template data structure by inspecting # each replacement field. - template = OrderedDict() + template = dict() for literal, field, fmt, conv in _FORMATTER.parse(format_string): pattern.write(re.escape(literal)) @@ -319,7 +318,6 @@ def __init__(self, format_string, flags=0): self._regex = re.compile("^" + pattern.getvalue() + "$", flags) def __call__(self, text): - # Try to match the text with the stored regex mobj = self._regex.search(text) if mobj is None: diff --git a/tests/stringparser_test.py b/tests/stringparser_test.py index 492598f..1e7f7c8 100644 --- a/tests/stringparser_test.py +++ b/tests/stringparser_test.py @@ -1,5 +1,4 @@ -import unittest -from collections import OrderedDict +import pytest import stringparser from stringparser import Parser @@ -9,281 +8,305 @@ class Dummy: pass -class ParserTest(unittest.TestCase): - def _test(self, fstring, value): - parser = Parser(fstring) - text = fstring.format(value) - self.assertEqual(parser(text), value) - - def _test_many(self, fstring, *value): - parser = Parser(fstring) - text = fstring.format(*value) - for out, val in zip(parser(text), value): - self.assertEqual(out, val) - - def _test_dict(self, fstring, **value): - parser = Parser(fstring) - text = fstring.format(**value) - self.assertEqual(set(parser(text).items()), set(OrderedDict(value).items())) - - def test_string(self): - "Parse single string" - self._test("before {0:s} after", "TEST") - - @unittest.expectedFailure - def test_string_failure(self): - "Unimplemented features for string" - self._test("{0:x<7s}", "result") - self._test("{0:x<8s}", "result") - self._test("{0: <7s}", "result") - self._test("{0:<7s}", "result") - self._test("{0:>7s}", "result") - self._test("{0:>8s}", "result") - self._test("{0:=8s}", "result") - self._test("{0:^8s}", "result") - self._test("{0:^9s}", "result") - self._test("{0:^10s}", "result") - - def test_escape_re_characters(self): - "Escape regex related characters" - self._test("start * | {0:s} [ ( * .after", "TEST") - - def test_int(self): - "Parse single int" - self._test("before {0:d} after", 42) - - @unittest.expectedFailure - def test_int_failure(self): - "Unimplemented features for int" - self._test("{0:()d}", -123) - - self._test("{0:1000d}", 100) - - self._test("{0:d}", 1) - self._test("{0:=d}", 1) - self._test("{0:^d}", 1) - self._test("{0:d}", -1) - self._test("{0:=d}", -1) - self._test("{0:^d}", -1) - - self._test("{0:<10d}", 0) - self._test("{0:<10d}", 123) - self._test("{0:<10d}", -123) - self._test("{0:>10d}", 123) - self._test("{0:>10d}", -123) - self._test("{0:^10d}", 123) - self._test("{0:^10d}", -123) - self._test("{0:=10d}", 123) - self._test("{0:=+10d}", 123) - self._test("{0:=10d}", -123) - self._test("{0:=+10d}", -123) - self._test("{0:=()10d}", 123) - - self._test("{0:=()10d}", -123) - self._test("{0:>()10d}", -123) - self._test("{0:<()10d}", -123) - self._test("{0:^()10d}", -123) - - self._test("{0:d}", 10**100) - self._test("{0:d}", -(10**100)) - self._test("{0:+d}", 10**100) - self._test("{0:()d}", -(10**100)) - self._test("{0:()110d}", -(10**100)) - self._test("{0:()110d}", -(10**100)) - - def test_binary(self): - "Parse single binary" - self._test("before {0:b} after", 42) - self._test("{0:b}", 0) - self._test("{0:b}", 42) - self._test("{0:b}", -42) - - self._test("{0:#b}", 42) - self._test("{0:#b}", -42) - - @unittest.expectedFailure - def test_binary_failure(self): - self._test("{0:<10b}", 0) - self._test("{0:>10b}", 0) - self._test("{0:<10b}", 9) - self._test("{0:>10b}", 9) - self._test("{0:^10b}", 9) - - self._test("{0:()b}", -(2**100 - 1)) - self._test("{0:=()200b}", -(2**100 - 1)) - - def test_octal(self): - "Parse single octal" - self._test("before {0:o} after", 42) - self._test("before {0:#o} after", 42) - self._test("before {0:o} after", -42) - self._test("before {0:#o} after", -42) - - def test_hex(self): - "Parse single hex (lower and uppercase)" - self._test("before {0:x} after", 42) - self._test("before {0:X} after", 42) - - self._test("before {0:#x} after", 42) - self._test("before {0:#X} after", 42) - - self._test("before {0:x} after", -42) - self._test("before {0:X} after", -42) - - self._test("before {0:#x} after", -42) - self._test("before {0:#X} after", -42) - - def test_fp_exp(self): - "Parse single floating point exponential format (lower and uppercase)" - self._test("before {0:e} after", 42.123e-10) - self._test("before {0:E} after", 42.123e-10) - - def test_fp_decimal(self): - "Parse single Floating point decimal format." - self._test("before {0:f} after", 42.123) - self._test("before {0:F} after", 42.123) - - def test_fp_auto(self): - """Floating point format. Uses exponential format if exponent is - greater than -4 or less than precision, decimal format otherwise. - """ - self._test("before {0:g} after", 42.123) - self._test("before {0:G} after", 42.123) - self._test("before {0:g} after", 42.123e-10) - self._test("before {0:G} after", 42.123e-10) - - def test_percent(self): - "Parse single percent" - self._test("before {0:%} after", 42) - - def test_unnamed(self): - self._test("before {:d} after", 42) - - def test_named(self): - self._test_dict("before {a:d} after", a=42) - self._test_dict("before {a:d} in between {b:d} after", a=42, b=23) - - @unittest.expectedFailure - def test_attributes(self): - h = Dummy() - h.first = "something" - h.seccond = "else" - - fmt = "before {0.first} after" - text = fmt.format(h) - obj = Parser(fmt)(text) - self.assertEqual(obj.first, h.first) - - fmt = "before {0.first} in between {0.second} after" - text = fmt.format(h) - obj = Parser(fmt)(text) - self.assertEqual(obj.first, h.first) - self.assertEqual(obj.second, h.second) - - fmt = "before {0.first} in between {1.second} after" - text = fmt.format(h) - obj1, obj2 = Parser(fmt)(text) - self.assertEqual(obj1.first, h.first) - self.assertEqual(obj2.second, h.second) - - def test_items(self): - h = dict() - h["first"] = "something" - h["second"] = "else" - - fmt = "before {0[first]} after" - text = fmt.format(h) - obj = Parser(fmt)(text) - self.assertEqual(obj["first"], h["first"]) - - fmt = "before {0[first]} in between {0[second]} after" - text = fmt.format(h) - obj = Parser(fmt)(text) - self.assertEqual(obj["first"], h["first"]) - self.assertEqual(obj["second"], h["second"]) - - fmt = "before {0[first]} in between {1[second]} after" - text = fmt.format(h, h) - obj1, obj2 = Parser(fmt)(text) - self.assertEqual(obj1["first"], h["first"]) - self.assertEqual(obj2["second"], h["second"]) - - def test_items_attributes(self): - h = Dummy() - h.first = {"second": "something"} - - fmt = "before {0.first[second]} after" - text = fmt.format(h) - obj = Parser(fmt)(text) - self.assertEqual(obj.first["second"], h.first["second"]) - - h = {"first": Dummy()} - h["first"].second = "something" - fmt = "before {0[first].second} after" - text = fmt.format(h) - obj = Parser(fmt)(text) - self.assertEqual(obj["first"].second, h["first"].second) - - def test_object_items(self): - h = dict() - h["aprop"] = "middle" - h["aprop2"] = "second" - - fmt = "before {0[aprop]} after" - text = fmt.format(h) - obj = Parser(fmt)(text) - self.assertEqual(obj["aprop"], h["aprop"]) - - fmt = "before {0[aprop]} in between {0[aprop2]} after" - text = fmt.format(h) - obj = Parser(fmt)(text) - self.assertEqual(obj["aprop"], h["aprop"]) - self.assertEqual(obj["aprop2"], h["aprop2"]) - - fmt = "before {0[aprop]} in between {1[aprop2]} after" - text = fmt.format(h, h) - obj1, obj2 = Parser(fmt)(text) - self.assertEqual(obj1["aprop"], h["aprop"]) - self.assertEqual(obj2["aprop2"], h["aprop2"]) - - def test_many_numbered(self): - "Parse many numbered" - self._test_many("before {0:d} after {1:d} end", 42, 43) - - def test_many_named(self): - "Parse many named" - self._test_dict("before {a:d} after {b:d} end", a=42, b=23) - - def test_many_scientific(self): - "Parse many scientific notation floats" - for format in "eEgG": - self._test_many( - "before {0:" + format + "} after {1:" + format + "} end", - 42.123e-10, - 78.532e25, - ) - - def test_many_unnamed(self): - "Parse many unnamed" - self._test_many("before {:d} after {:d} end", 42, 43) - - def test_ignore(self): - "Parse single" - parser = Parser("before {_:d} after") - self.assertEqual(parser("before 42 after"), OrderedDict()) - - def test_fail(self): - "Parse fail" - parser = Parser("before {:d} after") - self.assertRaises(ValueError, parser, "before bla after") - - def test_multiline(self): - "Test multipline" - parser = Parser("before {:d} after", stringparser.MULTILINE) - self.assertEqual(parser("bla\nbefore 42 after\nbla"), 42) - - -if __name__ == "__main__": - unittest.main() +def _test(fstring, value): + parser = Parser(fstring) + text = fstring.format(value) + assert parser(text) == value + + +def _test_many(fstring, *value): + parser = Parser(fstring) + text = fstring.format(*value) + for out, val in zip(parser(text), value): + assert out == val + + +def _test_dict(fstring, **value): + parser = Parser(fstring) + text = fstring.format(**value) + assert set(parser(text).items()) == set(dict(value).items()) + + +def test_string(): + "Parse single string" + _test("before {0:s} after", "TEST") + + +@pytest.mark.xfail +def test_string_failure(): + "Unimplemented features for string" + _test("{0:x<7s}", "result") + _test("{0:x<8s}", "result") + _test("{0: <7s}", "result") + _test("{0:<7s}", "result") + _test("{0:>7s}", "result") + _test("{0:>8s}", "result") + _test("{0:=8s}", "result") + _test("{0:^8s}", "result") + _test("{0:^9s}", "result") + _test("{0:^10s}", "result") + + +def test_escape_re_characters(): + "Escape regex related characters" + _test("start * | {0:s} [ ( * .after", "TEST") + + +def test_int(): + "Parse single int" + _test("before {0:d} after", 42) + + +@pytest.mark.xfail +def test_int_failure(): + "Unimplemented features for int" + _test("{0:()d}", -123) + + _test("{0:1000d}", 100) + + _test("{0:d}", 1) + _test("{0:=d}", 1) + _test("{0:^d}", 1) + _test("{0:d}", -1) + _test("{0:=d}", -1) + _test("{0:^d}", -1) + + _test("{0:<10d}", 0) + _test("{0:<10d}", 123) + _test("{0:<10d}", -123) + _test("{0:>10d}", 123) + _test("{0:>10d}", -123) + _test("{0:^10d}", 123) + _test("{0:^10d}", -123) + _test("{0:=10d}", 123) + _test("{0:=+10d}", 123) + _test("{0:=10d}", -123) + _test("{0:=+10d}", -123) + _test("{0:=()10d}", 123) + + _test("{0:=()10d}", -123) + _test("{0:>()10d}", -123) + _test("{0:<()10d}", -123) + _test("{0:^()10d}", -123) + + _test("{0:d}", 10**100) + _test("{0:d}", -(10**100)) + _test("{0:+d}", 10**100) + _test("{0:()d}", -(10**100)) + _test("{0:()110d}", -(10**100)) + _test("{0:()110d}", -(10**100)) + + +def test_binary(): + "Parse single binary" + _test("before {0:b} after", 42) + _test("{0:b}", 0) + _test("{0:b}", 42) + _test("{0:b}", -42) + + _test("{0:#b}", 42) + _test("{0:#b}", -42) + + +@pytest.mark.xfail +def test_binary_failure(): + _test("{0:<10b}", 0) + _test("{0:>10b}", 0) + _test("{0:<10b}", 9) + _test("{0:>10b}", 9) + _test("{0:^10b}", 9) + + _test("{0:()b}", -(2**100 - 1)) + _test("{0:=()200b}", -(2**100 - 1)) + + +def test_octal(): + "Parse single octal" + _test("before {0:o} after", 42) + _test("before {0:#o} after", 42) + _test("before {0:o} after", -42) + _test("before {0:#o} after", -42) + + +def test_hex(): + "Parse single hex (lower and uppercase)" + _test("before {0:x} after", 42) + _test("before {0:X} after", 42) + + _test("before {0:#x} after", 42) + _test("before {0:#X} after", 42) + + _test("before {0:x} after", -42) + _test("before {0:X} after", -42) + + _test("before {0:#x} after", -42) + _test("before {0:#X} after", -42) + + +def test_fp_exp(): + "Parse single floating point exponential format (lower and uppercase)" + _test("before {0:e} after", 42.123e-10) + _test("before {0:E} after", 42.123e-10) + + +def test_fp_decimal(): + "Parse single Floating point decimal format." + _test("before {0:f} after", 42.123) + _test("before {0:F} after", 42.123) + + +def test_fp_auto(): + """Floating point format. Uses exponential format if exponent is + greater than -4 or less than precision, decimal format otherwise. + """ + _test("before {0:g} after", 42.123) + _test("before {0:G} after", 42.123) + _test("before {0:g} after", 42.123e-10) + _test("before {0:G} after", 42.123e-10) + + +def test_percent(): + "Parse single percent" + _test("before {0:%} after", 42) + + +def test_unnamed(): + _test("before {:d} after", 42) + + +def test_named(): + _test_dict("before {a:d} after", a=42) + _test_dict("before {a:d} in between {b:d} after", a=42, b=23) + + +@pytest.mark.xfail +def test_attributes(): + h = Dummy() + h.first = "something" + h.seccond = "else" + + fmt = "before {0.first} after" + text = fmt.format(h) + obj = Parser(fmt)(text) + assert obj.first == h.first + + fmt = "before {0.first} in between {0.second} after" + text = fmt.format(h) + obj = Parser(fmt)(text) + assert obj.first == h.first + assert obj.second == h.second + + fmt = "before {0.first} in between {1.second} after" + text = fmt.format(h) + obj1, obj2 = Parser(fmt)(text) + assert obj1.first == h.first + assert obj2.second == h.second + + +def test_items(): + h = dict() + h["first"] = "something" + h["second"] = "else" + + fmt = "before {0[first]} after" + text = fmt.format(h) + obj = Parser(fmt)(text) + assert obj["first"] == h["first"] + + fmt = "before {0[first]} in between {0[second]} after" + text = fmt.format(h) + obj = Parser(fmt)(text) + assert obj["first"] == h["first"] + assert obj["second"] == h["second"] + + fmt = "before {0[first]} in between {1[second]} after" + text = fmt.format(h, h) + obj1, obj2 = Parser(fmt)(text) + assert obj1["first"] == h["first"] + assert obj2["second"] == h["second"] + + +def test_items_attributes(): + h = Dummy() + h.first = {"second": "something"} + + fmt = "before {0.first[second]} after" + text = fmt.format(h) + obj = Parser(fmt)(text) + assert obj.first["second"] == h.first["second"] + + h = {"first": Dummy()} + h["first"].second = "something" + fmt = "before {0[first].second} after" + text = fmt.format(h) + obj = Parser(fmt)(text) + assert obj["first"].second == h["first"].second + + +def test_object_items(): + h = dict() + h["aprop"] = "middle" + h["aprop2"] = "second" + + fmt = "before {0[aprop]} after" + text = fmt.format(h) + obj = Parser(fmt)(text) + assert obj["aprop"] == h["aprop"] + + fmt = "before {0[aprop]} in between {0[aprop2]} after" + text = fmt.format(h) + obj = Parser(fmt)(text) + assert obj["aprop"] == h["aprop"] + assert obj["aprop2"] == h["aprop2"] + + fmt = "before {0[aprop]} in between {1[aprop2]} after" + text = fmt.format(h, h) + obj1, obj2 = Parser(fmt)(text) + assert obj1["aprop"] == h["aprop"] + assert obj2["aprop2"] == h["aprop2"] + + +def test_many_numbered(): + "Parse many numbered" + _test_many("before {0:d} after {1:d} end", 42, 43) + + +def test_many_named(): + "Parse many named" + _test_dict("before {a:d} after {b:d} end", a=42, b=23) + + +def test_many_scientific(): + "Parse many scientific notation floats" + for format in "eEgG": + _test_many( + "before {0:" + format + "} after {1:" + format + "} end", + 42.123e-10, + 78.532e25, + ) + + +def test_many_unnamed(): + "Parse many unnamed" + _test_many("before {:d} after {:d} end", 42, 43) + + +def test_ignore(): + "Parse single" + parser = Parser("before {_:d} after") + assert parser("before 42 after") == dict() + + +def test_fail(): + "Parse fail" + parser = Parser("before {:d} after") + with pytest.raises(ValueError): + parser("before bla after") + + +def test_multiline(): + "Test multipline" + parser = Parser("before {:d} after", stringparser.MULTILINE) + assert parser("bla\nbefore 42 after\nbla") == 42