New Upstream Release - python-iniconfig
Ready changes
Summary
Merged new upstream version: 2.0.0 (was: 1.1.1).
Diff
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..a4451cd
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,63 @@
+name: Deploy
+
+on:
+ push:
+ branches:
+ - master
+ - "*deploy*"
+ release:
+ types:
+ - published
+
+jobs:
+ build:
+ if: github.repository == 'pytest-dev/iniconfig'
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Cache
+ uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: deploy-${{ hashFiles('**/pyproject.toml') }}
+ restore-keys: |
+ deploy-
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.x"
+
+ - name: Install build + twine
+ run: python -m pip install build twine setuptools_scm
+
+ - name: git describe output
+ run: git describe --tags
+
+ - id: scm_version
+ run: |
+ VERSION=$(python -m setuptools_scm --strip-dev)
+ echo SETUPTOOLS_SCM_PRETEND_VERSION=$VERSION >> $GITHUB_ENV
+
+ - name: Build package
+ run: python -m build
+
+ - name: twine check
+ run: twine check dist/*
+
+ - name: Publish package to PyPI
+ if: github.event.action == 'published'
+ uses: pypa/gh-action-pypi-publish@master
+ with:
+ user: __token__
+ password: ${{ secrets.pypi_password }}
+
+ - name: Publish package to TestPyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ user: __token__
+ password: ${{ secrets.test_pypi_password }}
+ repository_url: https://test.pypi.org/legacy/
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..524973f
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,32 @@
+name: build
+
+on: [push, pull_request]
+
+jobs:
+ build:
+
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ python: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ os: [ubuntu-latest, windows-latest]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python }}
+ - name: Install tox
+ run: python -m pip install --upgrade pip setuptools_scm hatch hatch-vcs
+ - name: install package local
+ run: pip install --no-build-isolation .
+
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
+ - uses: pre-commit/action@v3.0.0
diff --git a/.gitignore b/.gitignore
index 89e6234..f1a42cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ build/
dist/
__pycache__
.tox/
+src/iniconfig/_version.py
\ No newline at end of file
diff --git a/.hgignore b/.hgignore
deleted file mode 100644
index e8e91ab..0000000
--- a/.hgignore
+++ /dev/null
@@ -1,16 +0,0 @@
-
-# These lines are suggested according to the svn:ignore property
-# Feel free to enable them by uncommenting them
-syntax:glob
-*.pyc
-*.pyo
-*.swp
-*.html
-*.class
-
-.tox
-
-build
-dist
-*.egg-info
-
diff --git a/.landscape.yml b/.landscape.yml
deleted file mode 100644
index 5212dde..0000000
--- a/.landscape.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-pep8:
- full: true
-python-targets:
- - 2
- - 3
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..09cb80b
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,24 @@
+repos:
+- repo: https://github.com/asottile/pyupgrade
+ rev: v3.3.1
+ hooks:
+ - id: pyupgrade
+ args: [--py37-plus]
+- repo: https://github.com/tox-dev/pyproject-fmt
+ rev: "0.4.1"
+ hooks:
+ - id: pyproject-fmt
+
+- repo: https://github.com/psf/black
+ rev: 22.12.0
+ hooks:
+ - id: black
+ language_version: python3
+- repo: https://github.com/pre-commit/mirrors-mypy
+ rev: 'v0.991'
+ hooks:
+ - id: mypy
+ args: []
+ additional_dependencies:
+ - "pytest==7.2.0"
+ - "tomli"
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index e3fee06..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-language: python
-python:
-- '2.7'
-- '3.4'
-- '3.5'
-- nightly
-- pypy
-install: pip install setuptools_scm tox
-script: tox -e py
-deploy:
- provider: pypi
- user: ronny
- password:
- secure: DsRVX99HA6+3JoXOVP/nPXeabJy2P73ws7Ager/e4rx3p3jS74bId09XsBU46bAT9ANmRWPR8y5DRi5Zlq0WQ2uXoR55wmsdu2KUegk6bDIS4Iop8DFxY8Kjou9s8RZbDTP27LfuYXKMO1rDW/xa6EhiotYRodekeZUz3P3MYjIi6rBV2Rz3vwmInpkKOti7AFwAsCGmCCK13irmPJEp5nwl3RgeKu2AGaolw9eypJXeNLUcNDVQ88ZUUXQCkwgq7a1BkK6NMeQLMrWAE1bD3amCbVXHCR9TaVx1ZH1dnha5Jcfj3gEFucTmInWWes5u9rypvsCkSxKtSqdiUA7BMJq7XykV7nGNplGLm2sq4+KSYlf3gZXg4XNXQkNOi4EBtRvathfFziD2SZgdtjiQX2neh0dMjf9czc/uCYkKYCFLeozdw2oQQ+BsxhQfsmU2ILGCFHyFikmDbBqZOWfQE5TN3itQqV3TFK8sOHQ8iy3MDShs+lBk9AUwbCA5YbRh8hJKhgXyEsDpisC417Pj22+TbutTj7v3Rmpe/st4hoL740grWc3PSVUBaypG0RsoafSDZWnYnTC+0aakd6QEb5S9wnMkP94kijYjjF6yUInuT05wdbQv5XcSXqAdGzBqB5jNNdfwgWVCOlwGfjnvzKllhF3PmWPW/nfmQpGOQh4=
- on:
- tags: true
- distributions: sdist bdist_wheel
- repo: RonnyPfannschmidt/iniconfig
diff --git a/CHANGELOG b/CHANGELOG
index 679919f..5b7570a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,23 @@
+2.0.0
+======
+
+* add support for Python 3.7-3.11
+* drop support for Python 2.6-3.6
+* add encoding argument defaulting to utf-8
+* inline and clarify type annotations
+* move parsing code from inline to extra file
+* add typing overloads for helper methods
+
+
+.. note::
+
+ major release due to the major changes in python versions supported + changes in packaging
+
+ the api is expected to be compatible
+
+
1.1.1
-=========
+=====
* fix version determination (thanks @florimondmanca)
@@ -10,17 +28,19 @@
- ci fixes
1.0.1
-======
+=====
pytest 5+ support
1.0
-====
+===
- re-sync with pylib codebase
+- add support for Python 3.4-3.5
+- drop support for Python 2.4-2.5, 3.2
0.2
-==================
+===
- added ability to ask "name in iniconfig", i.e. to check
if a section is contained.
diff --git a/MANIFEST.in b/MANIFEST.in
index 06be514..badaa0c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,4 +2,3 @@ include LICENSE
include example.ini
include tox.ini
include src/iniconfig/py.typed
-recursive-include src *.pyi
diff --git a/README.txt b/README.rst
similarity index 88%
rename from README.txt
rename to README.rst
index 6bbad9a..948e883 100644
--- a/README.txt
+++ b/README.rst
@@ -4,7 +4,6 @@ iniconfig: brain-dead simple parsing of ini files
iniconfig is a small and simple INI-file parser module
having a unique set of features:
-* tested against Python2.4 across to Python3.2, Jython, PyPy
* maintains order of sections and entries
* supports multi-line values with or without line-continuations
* supports "#" comments everywhere
@@ -14,12 +13,14 @@ having a unique set of features:
If you encounter issues or have feature wishes please report them to:
- http://github.com/RonnyPfannschmidt/iniconfig/issues
+ https://github.com/RonnyPfannschmidt/iniconfig/issues
Basic Example
===================================
-If you have an ini file like this::
+If you have an ini file like this:
+
+.. code-block:: ini
# content of example.ini
[section1] # comment
@@ -31,7 +32,9 @@ If you have an ini file like this::
line1
line2
-then you can do::
+then you can do:
+
+.. code-block:: pycon
>>> import iniconfig
>>> ini = iniconfig.IniConfig("example.ini")
diff --git a/debian/changelog b/debian/changelog
index aac0a85..963ec09 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-iniconfig (2.0.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Mon, 07 Aug 2023 16:13:39 -0000
+
python-iniconfig (1.1.1-2) unstable; urgency=medium
[ Stefano Rivera ]
diff --git a/pyproject.toml b/pyproject.toml
index b2725d8..6eee6ef 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,70 @@
[build-system]
-requires = ["setuptools>=41.2.0", "wheel", "setuptools_scm>3"]
+build-backend = "hatchling.build"
+requires = [
+ "hatch-vcs",
+ "hatchling",
+]
+[project]
+name = "iniconfig"
+description = "brain-dead simple config-ini parsing"
+readme = "README.rst"
+license = "MIT"
+authors = [
+ { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" },
+ { name = "Holger Krekel", email = "holger.krekel@gmail.com" },
+]
+requires-python = ">=3.7"
+dynamic = [
+ "version",
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: MacOS :: MacOS X",
+ "Operating System :: Microsoft :: Windows",
+ "Operating System :: POSIX",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "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",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: Utilities",
+]
+[project.urls]
+Homepage = "https://github.com/pytest-dev/iniconfig"
-[tool.setuptools_scm]
\ No newline at end of file
+
+[tool.hatch.version]
+source = "vcs"
+
+[tool.hatch.build.hooks.vcs]
+version-file = "src/iniconfig/_version.py"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/src",
+]
+
+[tool.hatch.envs.test]
+dependencies = [
+ "pytest"
+]
+[tool.hatch.envs.test.scripts]
+default = "pytest"
+
+[[tool.hatch.envs.test.matrix]]
+python = ["3.7", "3.8", "3.9", "3.10", "3.11"]
+
+[tool.setuptools_scm]
+
+[tool.mypy]
+strict = true
+
+
+[tool.pytest.ini_options]
+testpaths = "testing"
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 3c6e79c..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[bdist_wheel]
-universal=1
diff --git a/setup.py b/setup.py
deleted file mode 100644
index f46f321..0000000
--- a/setup.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""
-iniconfig: brain-dead simple config-ini parsing.
-
-compatible CPython 2.3 through to CPython 3.2, Jython, PyPy
-
-(c) 2010 Ronny Pfannschmidt, Holger Krekel
-"""
-
-from setuptools import setup
-
-
-def main():
- with open('README.txt') as fp:
- readme = fp.read()
- setup(
- name='iniconfig',
- packages=['iniconfig'],
- package_dir={'': 'src'},
- description='iniconfig: brain-dead simple config-ini parsing',
- long_description=readme,
- use_scm_version=True,
- url='http://github.com/RonnyPfannschmidt/iniconfig',
- license='MIT License',
- platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
- author='Ronny Pfannschmidt, Holger Krekel',
- author_email=(
- 'opensource@ronnypfannschmidt.de, holger.krekel@gmail.com'),
- classifiers=[
- 'Development Status :: 4 - Beta',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: MIT License',
- 'Operating System :: POSIX',
- 'Operating System :: Microsoft :: Windows',
- 'Operating System :: MacOS :: MacOS X',
- 'Topic :: Software Development :: Libraries',
- 'Topic :: Utilities',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 3',
- ],
- include_package_data=True,
- zip_safe=False,
- )
-
-if __name__ == '__main__':
- main()
diff --git a/src/iniconfig/__init__.py b/src/iniconfig/__init__.py
index 6ad9eaf..c18a8e4 100644
--- a/src/iniconfig/__init__.py
+++ b/src/iniconfig/__init__.py
@@ -1,165 +1,216 @@
""" brain-dead simple parser for ini-style files.
(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed
"""
-__all__ = ['IniConfig', 'ParseError']
-
-COMMENTCHARS = "#;"
-
-
-class ParseError(Exception):
- def __init__(self, path, lineno, msg):
- Exception.__init__(self, path, lineno, msg)
- self.path = path
- self.lineno = lineno
- self.msg = msg
-
- def __str__(self):
- return "%s:%s: %s" % (self.path, self.lineno+1, self.msg)
-
-
-class SectionWrapper(object):
- def __init__(self, config, name):
+from __future__ import annotations
+from typing import (
+ Callable,
+ Iterator,
+ Mapping,
+ Optional,
+ Tuple,
+ TypeVar,
+ Union,
+ TYPE_CHECKING,
+ NoReturn,
+ NamedTuple,
+ overload,
+ cast,
+)
+
+import os
+
+if TYPE_CHECKING:
+ from typing_extensions import Final
+
+__all__ = ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"]
+
+from .exceptions import ParseError
+from . import _parse
+from ._parse import COMMENTCHARS, iscommentline
+
+_D = TypeVar("_D")
+_T = TypeVar("_T")
+
+
+class SectionWrapper:
+ config: Final[IniConfig]
+ name: Final[str]
+
+ def __init__(self, config: IniConfig, name: str) -> None:
self.config = config
self.name = name
- def lineof(self, name):
+ def lineof(self, name: str) -> int | None:
return self.config.lineof(self.name, name)
- def get(self, key, default=None, convert=str):
- return self.config.get(self.name, key,
- convert=convert, default=default)
-
- def __getitem__(self, key):
+ @overload
+ def get(self, key: str) -> str | None:
+ ...
+
+ @overload
+ def get(
+ self,
+ key: str,
+ convert: Callable[[str], _T],
+ ) -> _T | None:
+ ...
+
+ @overload
+ def get(
+ self,
+ key: str,
+ default: None,
+ convert: Callable[[str], _T],
+ ) -> _T | None:
+ ...
+
+ @overload
+ def get(self, key: str, default: _D, convert: None = None) -> str | _D:
+ ...
+
+ @overload
+ def get(
+ self,
+ key: str,
+ default: _D,
+ convert: Callable[[str], _T],
+ ) -> _T | _D:
+ ...
+
+ # TODO: investigate possible mypy bug wrt matching the passed over data
+ def get( # type: ignore [misc]
+ self,
+ key: str,
+ default: _D | None = None,
+ convert: Callable[[str], _T] | None = None,
+ ) -> _D | _T | str | None:
+ return self.config.get(self.name, key, convert=convert, default=default)
+
+ def __getitem__(self, key: str) -> str:
return self.config.sections[self.name][key]
- def __iter__(self):
- section = self.config.sections.get(self.name, [])
+ def __iter__(self) -> Iterator[str]:
+ section: Mapping[str, str] = self.config.sections.get(self.name, {})
- def lineof(key):
- return self.config.lineof(self.name, key)
- for name in sorted(section, key=lineof):
- yield name
+ def lineof(key: str) -> int:
+ return self.config.lineof(self.name, key) # type: ignore[return-value]
- def items(self):
+ yield from sorted(section, key=lineof)
+
+ def items(self) -> Iterator[tuple[str, str]]:
for name in self:
yield name, self[name]
-class IniConfig(object):
- def __init__(self, path, data=None):
- self.path = str(path) # convenience
+class IniConfig:
+ path: Final[str]
+ sections: Final[Mapping[str, Mapping[str, str]]]
+
+ def __init__(
+ self,
+ path: str | os.PathLike[str],
+ data: str | None = None,
+ encoding: str = "utf-8",
+ ) -> None:
+ self.path = os.fspath(path)
if data is None:
- f = open(self.path)
- try:
- tokens = self._parse(iter(f))
- finally:
- f.close()
- else:
- tokens = self._parse(data.splitlines(True))
+ with open(self.path, encoding=encoding) as fp:
+ data = fp.read()
+
+ tokens = _parse.parse_lines(self.path, data.splitlines(True))
self._sources = {}
- self.sections = {}
+ sections_data: dict[str, dict[str, str]]
+ self.sections = sections_data = {}
for lineno, section, name, value in tokens:
if section is None:
- self._raise(lineno, 'no section header defined')
+ raise ParseError(self.path, lineno, "no section header defined")
self._sources[section, name] = lineno
if name is None:
if section in self.sections:
- self._raise(lineno, 'duplicate section %r' % (section, ))
- self.sections[section] = {}
+ raise ParseError(
+ self.path, lineno, f"duplicate section {section!r}"
+ )
+ sections_data[section] = {}
else:
if name in self.sections[section]:
- self._raise(lineno, 'duplicate name %r' % (name, ))
- self.sections[section][name] = value
-
- def _raise(self, lineno, msg):
- raise ParseError(self.path, lineno, msg)
-
- def _parse(self, line_iter):
- result = []
- section = None
- for lineno, line in enumerate(line_iter):
- name, data = self._parseline(line, lineno)
- # new value
- if name is not None and data is not None:
- result.append((lineno, section, name, data))
- # new section
- elif name is not None and data is None:
- if not name:
- self._raise(lineno, 'empty section name')
- section = name
- result.append((lineno, section, None, None))
- # continuation
- elif name is None and data is not None:
- if not result:
- self._raise(lineno, 'unexpected value continuation')
- last = result.pop()
- last_name, last_data = last[-2:]
- if last_name is None:
- self._raise(lineno, 'unexpected value continuation')
-
- if last_data:
- data = '%s\n%s' % (last_data, data)
- result.append(last[:-1] + (data,))
- return result
-
- def _parseline(self, line, lineno):
- # blank lines
- if iscommentline(line):
- line = ""
- else:
- line = line.rstrip()
- if not line:
- return None, None
- # section
- if line[0] == '[':
- realline = line
- for c in COMMENTCHARS:
- line = line.split(c)[0].rstrip()
- if line[-1] == "]":
- return line[1:-1], None
- return None, realline.strip()
- # value
- elif not line[0].isspace():
- try:
- name, value = line.split('=', 1)
- if ":" in name:
- raise ValueError()
- except ValueError:
- try:
- name, value = line.split(":", 1)
- except ValueError:
- self._raise(lineno, 'unexpected line: %r' % line)
- return name.strip(), value.strip()
- # continuation
- else:
- return None, line.strip()
+ raise ParseError(self.path, lineno, f"duplicate name {name!r}")
+ assert value is not None
+ sections_data[section][name] = value
- def lineof(self, section, name=None):
+ def lineof(self, section: str, name: str | None = None) -> int | None:
lineno = self._sources.get((section, name))
- if lineno is not None:
- return lineno + 1
-
- def get(self, section, name, default=None, convert=str):
+ return None if lineno is None else lineno + 1
+
+ @overload
+ def get(
+ self,
+ section: str,
+ name: str,
+ ) -> str | None:
+ ...
+
+ @overload
+ def get(
+ self,
+ section: str,
+ name: str,
+ convert: Callable[[str], _T],
+ ) -> _T | None:
+ ...
+
+ @overload
+ def get(
+ self,
+ section: str,
+ name: str,
+ default: None,
+ convert: Callable[[str], _T],
+ ) -> _T | None:
+ ...
+
+ @overload
+ def get(
+ self, section: str, name: str, default: _D, convert: None = None
+ ) -> str | _D:
+ ...
+
+ @overload
+ def get(
+ self,
+ section: str,
+ name: str,
+ default: _D,
+ convert: Callable[[str], _T],
+ ) -> _T | _D:
+ ...
+
+ def get( # type: ignore
+ self,
+ section: str,
+ name: str,
+ default: _D | None = None,
+ convert: Callable[[str], _T] | None = None,
+ ) -> _D | _T | str | None:
try:
- return convert(self.sections[section][name])
+ value: str = self.sections[section][name]
except KeyError:
return default
+ else:
+ if convert is not None:
+ return convert(value)
+ else:
+ return value
- def __getitem__(self, name):
+ def __getitem__(self, name: str) -> SectionWrapper:
if name not in self.sections:
raise KeyError(name)
return SectionWrapper(self, name)
- def __iter__(self):
- for name in sorted(self.sections, key=self.lineof):
+ def __iter__(self) -> Iterator[SectionWrapper]:
+ for name in sorted(self.sections, key=self.lineof): # type: ignore
yield SectionWrapper(self, name)
- def __contains__(self, arg):
+ def __contains__(self, arg: str) -> bool:
return arg in self.sections
-
-
-def iscommentline(line):
- c = line.lstrip()[:1]
- return c in COMMENTCHARS
diff --git a/src/iniconfig/__init__.pyi b/src/iniconfig/__init__.pyi
deleted file mode 100644
index b6284be..0000000
--- a/src/iniconfig/__init__.pyi
+++ /dev/null
@@ -1,31 +0,0 @@
-from typing import Callable, Iterator, Mapping, Optional, Tuple, TypeVar, Union
-from typing_extensions import Final
-
-_D = TypeVar('_D')
-_T = TypeVar('_T')
-
-class ParseError(Exception):
- # Private __init__.
- path: Final[str]
- lineno: Final[int]
- msg: Final[str]
-
-class SectionWrapper:
- # Private __init__.
- config: Final[IniConfig]
- name: Final[str]
- def __getitem__(self, key: str) -> str: ...
- def __iter__(self) -> Iterator[str]: ...
- def get(self, key: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ...
- def items(self) -> Iterator[Tuple[str, str]]: ...
- def lineof(self, name: str) -> Optional[int]: ...
-
-class IniConfig:
- path: Final[str]
- sections: Final[Mapping[str, Mapping[str, str]]]
- def __init__(self, path: str, data: Optional[str] = None): ...
- def __contains__(self, arg: str) -> bool: ...
- def __getitem__(self, name: str) -> SectionWrapper: ...
- def __iter__(self) -> Iterator[SectionWrapper]: ...
- def get(self, section: str, name: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ...
- def lineof(self, section: str, name: Optional[str] = ...) -> Optional[int]: ...
diff --git a/src/iniconfig/_parse.py b/src/iniconfig/_parse.py
new file mode 100644
index 0000000..2d03437
--- /dev/null
+++ b/src/iniconfig/_parse.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+from .exceptions import ParseError
+
+from typing import NamedTuple
+
+
+COMMENTCHARS = "#;"
+
+
+class _ParsedLine(NamedTuple):
+ lineno: int
+ section: str | None
+ name: str | None
+ value: str | None
+
+
+def parse_lines(path: str, line_iter: list[str]) -> list[_ParsedLine]:
+ result: list[_ParsedLine] = []
+ section = None
+ for lineno, line in enumerate(line_iter):
+ name, data = _parseline(path, line, lineno)
+ # new value
+ if name is not None and data is not None:
+ result.append(_ParsedLine(lineno, section, name, data))
+ # new section
+ elif name is not None and data is None:
+ if not name:
+ raise ParseError(path, lineno, "empty section name")
+ section = name
+ result.append(_ParsedLine(lineno, section, None, None))
+ # continuation
+ elif name is None and data is not None:
+ if not result:
+ raise ParseError(path, lineno, "unexpected value continuation")
+ last = result.pop()
+ if last.name is None:
+ raise ParseError(path, lineno, "unexpected value continuation")
+
+ if last.value:
+ last = last._replace(value=f"{last.value}\n{data}")
+ else:
+ last = last._replace(value=data)
+ result.append(last)
+ return result
+
+
+def _parseline(path: str, line: str, lineno: int) -> tuple[str | None, str | None]:
+ # blank lines
+ if iscommentline(line):
+ line = ""
+ else:
+ line = line.rstrip()
+ if not line:
+ return None, None
+ # section
+ if line[0] == "[":
+ realline = line
+ for c in COMMENTCHARS:
+ line = line.split(c)[0].rstrip()
+ if line[-1] == "]":
+ return line[1:-1], None
+ return None, realline.strip()
+ # value
+ elif not line[0].isspace():
+ try:
+ name, value = line.split("=", 1)
+ if ":" in name:
+ raise ValueError()
+ except ValueError:
+ try:
+ name, value = line.split(":", 1)
+ except ValueError:
+ raise ParseError(path, lineno, "unexpected line: %r" % line)
+ return name.strip(), value.strip()
+ # continuation
+ else:
+ return None, line.strip()
+
+
+def iscommentline(line: str) -> bool:
+ c = line.lstrip()[:1]
+ return c in COMMENTCHARS
diff --git a/src/iniconfig/exceptions.py b/src/iniconfig/exceptions.py
new file mode 100644
index 0000000..bc898e6
--- /dev/null
+++ b/src/iniconfig/exceptions.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing_extensions import Final
+
+
+class ParseError(Exception):
+ path: Final[str]
+ lineno: Final[int]
+ msg: Final[str]
+
+ def __init__(self, path: str, lineno: int, msg: str) -> None:
+ super().__init__(path, lineno, msg)
+ self.path = path
+ self.lineno = lineno
+ self.msg = msg
+
+ def __str__(self) -> str:
+ return f"{self.path}:{self.lineno + 1}: {self.msg}"
diff --git a/testing/conftest.py b/testing/conftest.py
index d265a29..f122ddd 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -1,2 +1 @@
-
option_doctestglob = "README.txt"
diff --git a/testing/test_iniconfig.py b/testing/test_iniconfig.py
index fe12421..9e94b28 100644
--- a/testing/test_iniconfig.py
+++ b/testing/test_iniconfig.py
@@ -1,282 +1,268 @@
-import py
+from __future__ import annotations
import pytest
from iniconfig import IniConfig, ParseError, __all__ as ALL
+from iniconfig._parse import _ParsedLine as PL
from iniconfig import iscommentline
from textwrap import dedent
+from pathlib import Path
-check_tokens = {
- 'section': (
- '[section]',
- [(0, 'section', None, None)]
+check_tokens: dict[str, tuple[str, list[PL]]] = {
+ "section": ("[section]", [PL(0, "section", None, None)]),
+ "value": ("value = 1", [PL(0, None, "value", "1")]),
+ "value in section": (
+ "[section]\nvalue=1",
+ [PL(0, "section", None, None), PL(1, "section", "value", "1")],
),
- 'value': (
- 'value = 1',
- [(0, None, 'value', '1')]
+ "value with continuation": (
+ "names =\n Alice\n Bob",
+ [PL(0, None, "names", "Alice\nBob")],
),
- 'value in section': (
- '[section]\nvalue=1',
- [(0, 'section', None, None), (1, 'section', 'value', '1')]
+ "value with aligned continuation": (
+ "names = Alice\n Bob",
+ [PL(0, None, "names", "Alice\nBob")],
),
- 'value with continuation': (
- 'names =\n Alice\n Bob',
- [(0, None, 'names', 'Alice\nBob')]
+ "blank line": (
+ "[section]\n\nvalue=1",
+ [PL(0, "section", None, None), PL(2, "section", "value", "1")],
),
- 'value with aligned continuation': (
- 'names = Alice\n'
- ' Bob',
- [(0, None, 'names', 'Alice\nBob')]
+ "comment": ("# comment", []),
+ "comment on value": ("value = 1", [PL(0, None, "value", "1")]),
+ "comment on section": ("[section] #comment", [PL(0, "section", None, None)]),
+ "comment2": ("; comment", []),
+ "comment2 on section": ("[section] ;comment", [PL(0, "section", None, None)]),
+ "pseudo section syntax in value": (
+ "name = value []",
+ [PL(0, None, "name", "value []")],
),
- 'blank line': (
- '[section]\n\nvalue=1',
- [(0, 'section', None, None), (2, 'section', 'value', '1')]
- ),
- 'comment': (
- '# comment',
- []
- ),
- 'comment on value': (
- 'value = 1',
- [(0, None, 'value', '1')]
- ),
-
- 'comment on section': (
- '[section] #comment',
- [(0, 'section', None, None)]
- ),
- 'comment2': (
- '; comment',
- []
- ),
-
- 'comment2 on section': (
- '[section] ;comment',
- [(0, 'section', None, None)]
- ),
- 'pseudo section syntax in value': (
- 'name = value []',
- [(0, None, 'name', 'value []')]
- ),
- 'assignment in value': (
- 'value = x = 3',
- [(0, None, 'value', 'x = 3')]
- ),
- 'use of colon for name-values': (
- 'name: y',
- [(0, None, 'name', 'y')]
- ),
- 'use of colon without space': (
- 'value:y=5',
- [(0, None, 'value', 'y=5')]
- ),
- 'equality gets precedence': (
- 'value=xyz:5',
- [(0, None, 'value', 'xyz:5')]
- ),
-
+ "assignment in value": ("value = x = 3", [PL(0, None, "value", "x = 3")]),
+ "use of colon for name-values": ("name: y", [PL(0, None, "name", "y")]),
+ "use of colon without space": ("value:y=5", [PL(0, None, "value", "y=5")]),
+ "equality gets precedence": ("value=xyz:5", [PL(0, None, "value", "xyz:5")]),
}
@pytest.fixture(params=sorted(check_tokens))
-def input_expected(request):
+def input_expected(request: pytest.FixtureRequest) -> tuple[str, list[PL]]:
+
return check_tokens[request.param]
@pytest.fixture
-def input(input_expected):
+def input(input_expected: tuple[str, list[PL]]) -> str:
return input_expected[0]
@pytest.fixture
-def expected(input_expected):
+def expected(input_expected: tuple[str, list[PL]]) -> list[PL]:
return input_expected[1]
-def parse(input):
- # only for testing purposes - _parse() does not use state except path
- ini = object.__new__(IniConfig)
- ini.path = "sample"
- return ini._parse(input.splitlines(True))
+def parse(input: str) -> list[PL]:
+ from iniconfig._parse import parse_lines
+
+ return parse_lines("sample", input.splitlines(True))
-def parse_a_error(input):
- return py.test.raises(ParseError, parse, input)
+def parse_a_error(input: str) -> ParseError:
+ try:
+ parse(input)
+ except ParseError as e:
+ return e
+ else:
+ raise ValueError(input)
-def test_tokenize(input, expected):
+def test_tokenize(input: str, expected: list[PL]) -> None:
parsed = parse(input)
assert parsed == expected
-def test_parse_empty():
+def test_parse_empty() -> None:
parsed = parse("")
assert not parsed
ini = IniConfig("sample", "")
assert not ini.sections
-def test_ParseError():
+def test_ParseError() -> None:
e = ParseError("filename", 0, "hello")
assert str(e) == "filename:1: hello"
-def test_continuation_needs_perceeding_token():
- excinfo = parse_a_error(' Foo')
- assert excinfo.value.lineno == 0
+def test_continuation_needs_perceeding_token() -> None:
+ err = parse_a_error(" Foo")
+ assert err.lineno == 0
-def test_continuation_cant_be_after_section():
- excinfo = parse_a_error('[section]\n Foo')
- assert excinfo.value.lineno == 1
+def test_continuation_cant_be_after_section() -> None:
+ err = parse_a_error("[section]\n Foo")
+ assert err.lineno == 1
-def test_section_cant_be_empty():
- excinfo = parse_a_error('[]')
- assert excinfo.value.lineno == 0
+def test_section_cant_be_empty() -> None:
+ err = parse_a_error("[]")
+ assert err.lineno == 0
-@py.test.mark.parametrize('line', [
- '!!',
- ])
-def test_error_on_weird_lines(line):
+@pytest.mark.parametrize(
+ "line",
+ [
+ "!!",
+ ],
+)
+def test_error_on_weird_lines(line: str) -> None:
parse_a_error(line)
-def test_iniconfig_from_file(tmpdir):
- path = tmpdir/'test.txt'
- path.write('[metadata]\nname=1')
+def test_iniconfig_from_file(tmp_path: Path) -> None:
+ path = tmp_path / "test.txt"
+ path.write_text("[metadata]\nname=1")
- config = IniConfig(path=path)
- assert list(config.sections) == ['metadata']
- config = IniConfig(path, "[diff]")
- assert list(config.sections) == ['diff']
+ config = IniConfig(path=str(path))
+ assert list(config.sections) == ["metadata"]
+ config = IniConfig(str(path), "[diff]")
+ assert list(config.sections) == ["diff"]
with pytest.raises(TypeError):
- IniConfig(data=path.read())
+ IniConfig(data=path.read_text()) # type: ignore
-def test_iniconfig_section_first(tmpdir):
+def test_iniconfig_section_first() -> None:
with pytest.raises(ParseError) as excinfo:
- IniConfig("x", data='name=1')
+ IniConfig("x", data="name=1")
assert excinfo.value.msg == "no section header defined"
-def test_iniconig_section_duplicate_fails():
+def test_iniconig_section_duplicate_fails() -> None:
with pytest.raises(ParseError) as excinfo:
- IniConfig("x", data='[section]\n[section]')
- assert 'duplicate section' in str(excinfo.value)
+ IniConfig("x", data="[section]\n[section]")
+ assert "duplicate section" in str(excinfo.value)
-def test_iniconfig_duplicate_key_fails():
+def test_iniconfig_duplicate_key_fails() -> None:
with pytest.raises(ParseError) as excinfo:
- IniConfig("x", data='[section]\nname = Alice\nname = bob')
+ IniConfig("x", data="[section]\nname = Alice\nname = bob")
- assert 'duplicate name' in str(excinfo.value)
+ assert "duplicate name" in str(excinfo.value)
-def test_iniconfig_lineof():
- config = IniConfig("x.ini", data=(
- '[section]\n'
- 'value = 1\n'
- '[section2]\n'
- '# comment\n'
- 'value =2'
- ))
+def test_iniconfig_lineof() -> None:
+ config = IniConfig(
+ "x.ini",
+ data=("[section]\nvalue = 1\n[section2]\n# comment\nvalue =2"),
+ )
- assert config.lineof('missing') is None
- assert config.lineof('section') == 1
- assert config.lineof('section2') == 3
- assert config.lineof('section', 'value') == 2
- assert config.lineof('section2', 'value') == 5
+ assert config.lineof("missing") is None
+ assert config.lineof("section") == 1
+ assert config.lineof("section2") == 3
+ assert config.lineof("section", "value") == 2
+ assert config.lineof("section2", "value") == 5
- assert config['section'].lineof('value') == 2
- assert config['section2'].lineof('value') == 5
+ assert config["section"].lineof("value") == 2
+ assert config["section2"].lineof("value") == 5
-def test_iniconfig_get_convert():
- config = IniConfig("x", data='[section]\nint = 1\nfloat = 1.1')
- assert config.get('section', 'int') == '1'
- assert config.get('section', 'int', convert=int) == 1
+def test_iniconfig_get_convert() -> None:
+ config = IniConfig("x", data="[section]\nint = 1\nfloat = 1.1")
+ assert config.get("section", "int") == "1"
+ assert config.get("section", "int", convert=int) == 1
-def test_iniconfig_get_missing():
- config = IniConfig("x", data='[section]\nint = 1\nfloat = 1.1')
- assert config.get('section', 'missing', default=1) == 1
- assert config.get('section', 'missing') is None
+def test_iniconfig_get_missing() -> None:
+ config = IniConfig("x", data="[section]\nint = 1\nfloat = 1.1")
+ assert config.get("section", "missing", default=1) == 1
+ assert config.get("section", "missing") is None
-def test_section_get():
- config = IniConfig("x", data='[section]\nvalue=1')
- section = config['section']
- assert section.get('value', convert=int) == 1
- assert section.get('value', 1) == "1"
- assert section.get('missing', 2) == 2
+def test_section_get() -> None:
+ config = IniConfig("x", data="[section]\nvalue=1")
+ section = config["section"]
+ assert section.get("value", convert=int) == 1
+ assert section.get("value", 1) == "1"
+ assert section.get("missing", 2) == 2
-def test_missing_section():
- config = IniConfig("x", data='[section]\nvalue=1')
+def test_missing_section() -> None:
+ config = IniConfig("x", data="[section]\nvalue=1")
with pytest.raises(KeyError):
- config["other"]
+ config["other"]
-def test_section_getitem():
- config = IniConfig("x", data='[section]\nvalue=1')
- assert config['section']['value'] == '1'
- assert config['section']['value'] == '1'
+def test_section_getitem() -> None:
+ config = IniConfig("x", data="[section]\nvalue=1")
+ assert config["section"]["value"] == "1"
+ assert config["section"]["value"] == "1"
-def test_section_iter():
- config = IniConfig("x", data='[section]\nvalue=1')
- names = list(config['section'])
- assert names == ['value']
- items = list(config['section'].items())
- assert items == [('value', '1')]
+def test_section_iter() -> None:
+ config = IniConfig("x", data="[section]\nvalue=1")
+ names = list(config["section"])
+ assert names == ["value"]
+ items = list(config["section"].items())
+ assert items == [("value", "1")]
-def test_config_iter():
- config = IniConfig("x.ini", data=dedent('''
+def test_config_iter() -> None:
+ config = IniConfig(
+ "x.ini",
+ data=dedent(
+ """
[section1]
value=1
[section2]
value=2
- '''))
+ """
+ ),
+ )
l = list(config)
assert len(l) == 2
- assert l[0].name == 'section1'
- assert l[0]['value'] == '1'
- assert l[1].name == 'section2'
- assert l[1]['value'] == '2'
+ assert l[0].name == "section1"
+ assert l[0]["value"] == "1"
+ assert l[1].name == "section2"
+ assert l[1]["value"] == "2"
-def test_config_contains():
- config = IniConfig("x.ini", data=dedent('''
+def test_config_contains() -> None:
+ config = IniConfig(
+ "x.ini",
+ data=dedent(
+ """
[section1]
value=1
[section2]
value=2
- '''))
- assert 'xyz' not in config
- assert 'section1' in config
- assert 'section2' in config
-
-
-def test_iter_file_order():
- config = IniConfig("x.ini", data="""
+ """
+ ),
+ )
+ assert "xyz" not in config
+ assert "section1" in config
+ assert "section2" in config
+
+
+def test_iter_file_order() -> None:
+ config = IniConfig(
+ "x.ini",
+ data="""
[section2] #cpython dict ordered before section
value = 1
value2 = 2 # dict ordered before value
[section]
a = 1
b = 2
-""")
+""",
+ )
l = list(config)
secnames = [x.name for x in l]
- assert secnames == ['section2', 'section']
- assert list(config['section2']) == ['value', 'value2']
- assert list(config['section']) == ['a', 'b']
+ assert secnames == ["section2", "section"]
+ assert list(config["section2"]) == ["value", "value2"]
+ assert list(config["section"]) == ["a", "b"]
-def test_example_pypirc():
- config = IniConfig("pypirc", data=dedent('''
+def test_example_pypirc() -> None:
+ config = IniConfig(
+ "pypirc",
+ data=dedent(
+ """
[distutils]
index-servers =
pypi
@@ -291,24 +277,29 @@ def test_example_pypirc():
repository: http://example.com/pypi
username: <username>
password: <password>
- '''))
+ """
+ ),
+ )
distutils, pypi, other = list(config)
assert distutils["index-servers"] == "pypi\nother"
- assert pypi['repository'] == '<repository-url>'
- assert pypi['username'] == '<username>'
- assert pypi['password'] == '<password>'
- assert ['repository', 'username', 'password'] == list(other)
-
-
-def test_api_import():
- assert ALL == ['IniConfig', 'ParseError']
-
-
-@pytest.mark.parametrize("line", [
- "#qwe",
- " #qwe",
- ";qwe",
- " ;qwe",
-])
-def test_iscommentline_true(line):
+ assert pypi["repository"] == "<repository-url>"
+ assert pypi["username"] == "<username>"
+ assert pypi["password"] == "<password>"
+ assert ["repository", "username", "password"] == list(other)
+
+
+def test_api_import() -> None:
+ assert ALL == ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"]
+
+
+@pytest.mark.parametrize(
+ "line",
+ [
+ "#qwe",
+ " #qwe",
+ ";qwe",
+ " ;qwe",
+ ],
+)
+def test_iscommentline_true(line: str) -> None:
assert iscommentline(line)
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 298838b..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,14 +0,0 @@
-[tox]
-envlist=py27,py26,py33,py34,py35
-
-
-[testenv]
-commands=
- pytest {posargs}
-deps=
- pytest
-
-
-[pytest]
-testpaths=
- testing