pax_global_header00006660000000000000000000000064147665772730014542gustar00rootroot0000000000000052 comment=34793a6417e35e511d0c17ffdd8a02ade0f9568e iniconfig-2.1.0/000077500000000000000000000000001476657727300135075ustar00rootroot00000000000000iniconfig-2.1.0/.github/000077500000000000000000000000001476657727300150475ustar00rootroot00000000000000iniconfig-2.1.0/.github/workflows/000077500000000000000000000000001476657727300171045ustar00rootroot00000000000000iniconfig-2.1.0/.github/workflows/deploy.yml000066400000000000000000000030031476657727300211170ustar00rootroot00000000000000name: 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@v4 with: fetch-depth: 0 - name: Cache uses: actions/cache@v4 with: path: ~/.cache/pip key: deploy-${{ hashFiles('**/pyproject.toml') }} restore-keys: | deploy- - name: Set up Python uses: actions/setup-python@v5 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@release/v1 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/ iniconfig-2.1.0/.github/workflows/main.yml000066400000000000000000000015061476657727300205550ustar00rootroot00000000000000name: build on: [push, pull_request, workflow_dispatch] jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install hatch run: python -m pip install --upgrade pip hatch hatch-vcs - name: Run tests run: hatch run +py=${{ matrix.python }} test:default --color=yes pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.x - uses: pre-commit/action@v3.0.1 iniconfig-2.1.0/.gitignore000066400000000000000000000001301476657727300154710ustar00rootroot00000000000000*.egg-info *.pyc .cache/ .eggs/ build/ dist/ __pycache__ .tox/ src/iniconfig/_version.pyiniconfig-2.1.0/.pre-commit-config.yaml000066400000000000000000000010341476657727300177660ustar00rootroot00000000000000repos: - repo: https://github.com/asottile/pyupgrade rev: v3.3.1 hooks: - id: pyupgrade args: [--py38-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"iniconfig-2.1.0/CHANGELOG000066400000000000000000000021161476657727300147210ustar00rootroot000000000000002.1.0 ===== * fix artifact building - pin minimal version of hatch * drop eol python 3.8 * add python 3.12 and 3.13 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) 1.1.0 ===== - typing stubs (thanks @bluetech) - 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. - fix bug in "name=value" parsing where value was "x=3" - allow for ': ' to delimit name=value pairs, so that e.g. .pypirc files like http://docs.python.org/distutils/packageindex.html can be successfully parsed iniconfig-2.1.0/LICENSE000066400000000000000000000021121476657727300145100ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2010 - 2023 Holger Krekel and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. iniconfig-2.1.0/MANIFEST.in000066400000000000000000000001231476657727300152410ustar00rootroot00000000000000include LICENSE include example.ini include tox.ini include src/iniconfig/py.typed iniconfig-2.1.0/README.rst000066400000000000000000000027361476657727300152060ustar00rootroot00000000000000iniconfig: brain-dead simple parsing of ini files ======================================================= iniconfig is a small and simple INI-file parser module having a unique set of features: * maintains order of sections and entries * supports multi-line values with or without line-continuations * supports "#" comments everywhere * raises errors with proper line-numbers * no bells and whistles like automatic substitutions * iniconfig raises an Error if two sections have the same name. If you encounter issues or have feature wishes please report them to: https://github.com/RonnyPfannschmidt/iniconfig/issues Basic Example =================================== If you have an ini file like this: .. code-block:: ini # content of example.ini [section1] # comment name1=value1 # comment name1b=value1,value2 # comment [section2] name2= line1 line2 then you can do: .. code-block:: pycon >>> import iniconfig >>> ini = iniconfig.IniConfig("example.ini") >>> ini['section1']['name1'] # raises KeyError if not exists 'value1' >>> ini.get('section1', 'name1b', [], lambda x: x.split(",")) ['value1', 'value2'] >>> ini.get('section1', 'notexist', [], lambda x: x.split(",")) [] >>> [x.name for x in list(ini)] ['section1', 'section2'] >>> list(list(ini)[0].items()) [('name1', 'value1'), ('name1b', 'value1,value2')] >>> 'section1' in ini True >>> 'inexistendsection' in ini False iniconfig-2.1.0/example.ini000066400000000000000000000002161476657727300156420ustar00rootroot00000000000000 # content of example.ini [section1] # comment name1=value1 # comment name1b=value1,value2 # comment [section2] name2= line1 line2 iniconfig-2.1.0/pyproject.toml000066400000000000000000000032241476657727300164240ustar00rootroot00000000000000[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs", "hatchling>=1.26", ] [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.8" 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.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries", "Topic :: Utilities", ] [project.urls] Homepage = "https://github.com/pytest-dev/iniconfig" [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 {args}" [[tool.hatch.envs.test.matrix]] python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] [tool.setuptools_scm] [tool.mypy] strict = true [tool.pytest.ini_options] testpaths = "testing" iniconfig-2.1.0/src/000077500000000000000000000000001476657727300142765ustar00rootroot00000000000000iniconfig-2.1.0/src/iniconfig/000077500000000000000000000000001476657727300162435ustar00rootroot00000000000000iniconfig-2.1.0/src/iniconfig/__init__.py000066400000000000000000000125261476657727300203620ustar00rootroot00000000000000""" brain-dead simple parser for ini-style files. (C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed """ 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 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: str) -> int | None: return self.config.lineof(self.name, name) @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) -> Iterator[str]: section: Mapping[str, str] = self.config.sections.get(self.name, {}) def lineof(key: str) -> int: return self.config.lineof(self.name, key) # type: ignore[return-value] yield from sorted(section, key=lineof) def items(self) -> Iterator[tuple[str, str]]: for name in self: yield name, self[name] 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: with open(self.path, encoding=encoding) as fp: data = fp.read() tokens = _parse.parse_lines(self.path, data.splitlines(True)) self._sources = {} sections_data: dict[str, dict[str, str]] self.sections = sections_data = {} for lineno, section, name, value in tokens: if section is None: raise ParseError(self.path, lineno, "no section header defined") self._sources[section, name] = lineno if name is None: if section in self.sections: raise ParseError( self.path, lineno, f"duplicate section {section!r}" ) sections_data[section] = {} else: if name in self.sections[section]: raise ParseError(self.path, lineno, f"duplicate name {name!r}") assert value is not None sections_data[section][name] = value def lineof(self, section: str, name: str | None = None) -> int | None: lineno = self._sources.get((section, name)) 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: 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: str) -> SectionWrapper: if name not in self.sections: raise KeyError(name) return SectionWrapper(self, name) def __iter__(self) -> Iterator[SectionWrapper]: for name in sorted(self.sections, key=self.lineof): # type: ignore yield SectionWrapper(self, name) def __contains__(self, arg: str) -> bool: return arg in self.sections iniconfig-2.1.0/src/iniconfig/_parse.py000066400000000000000000000046041476657727300200720ustar00rootroot00000000000000from __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 iniconfig-2.1.0/src/iniconfig/exceptions.py000066400000000000000000000007521476657727300210020ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from typing 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}" iniconfig-2.1.0/src/iniconfig/py.typed000066400000000000000000000000001476657727300177300ustar00rootroot00000000000000iniconfig-2.1.0/testing/000077500000000000000000000000001476657727300151645ustar00rootroot00000000000000iniconfig-2.1.0/testing/conftest.py000066400000000000000000000000421476657727300173570ustar00rootroot00000000000000option_doctestglob = "README.txt" iniconfig-2.1.0/testing/test_iniconfig.py000066400000000000000000000200051476657727300205370ustar00rootroot00000000000000from __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: 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 with continuation": ( "names =\n Alice\n Bob", [PL(0, None, "names", "Alice\nBob")], ), "value with aligned continuation": ( "names = Alice\n Bob", [PL(0, None, "names", "Alice\nBob")], ), "blank line": ( "[section]\n\nvalue=1", [PL(0, "section", None, None), PL(2, "section", "value", "1")], ), "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 []")], ), "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: pytest.FixtureRequest) -> tuple[str, list[PL]]: return check_tokens[request.param] @pytest.fixture def input(input_expected: tuple[str, list[PL]]) -> str: return input_expected[0] @pytest.fixture def expected(input_expected: tuple[str, list[PL]]) -> list[PL]: return input_expected[1] def parse(input: str) -> list[PL]: from iniconfig._parse import parse_lines return parse_lines("sample", input.splitlines(True)) def parse_a_error(input: str) -> ParseError: try: parse(input) except ParseError as e: return e else: raise ValueError(input) def test_tokenize(input: str, expected: list[PL]) -> None: parsed = parse(input) assert parsed == expected def test_parse_empty() -> None: parsed = parse("") assert not parsed ini = IniConfig("sample", "") assert not ini.sections def test_ParseError() -> None: e = ParseError("filename", 0, "hello") assert str(e) == "filename:1: hello" def test_continuation_needs_perceeding_token() -> None: err = parse_a_error(" Foo") assert err.lineno == 0 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() -> None: err = parse_a_error("[]") assert err.lineno == 0 @pytest.mark.parametrize( "line", [ "!!", ], ) def test_error_on_weird_lines(line: str) -> None: parse_a_error(line) def test_iniconfig_from_file(tmp_path: Path) -> None: path = tmp_path / "test.txt" path.write_text("[metadata]\nname=1") 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_text()) # type: ignore def test_iniconfig_section_first() -> None: with pytest.raises(ParseError) as excinfo: IniConfig("x", data="name=1") assert excinfo.value.msg == "no section header defined" 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) def test_iniconfig_duplicate_key_fails() -> None: with pytest.raises(ParseError) as excinfo: IniConfig("x", data="[section]\nname = Alice\nname = bob") assert "duplicate name" in str(excinfo.value) 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["section"].lineof("value") == 2 assert config["section2"].lineof("value") == 5 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() -> 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() -> 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() -> None: config = IniConfig("x", data="[section]\nvalue=1") with pytest.raises(KeyError): config["other"] 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() -> 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() -> 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" 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() -> 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"] def test_example_pypirc() -> None: config = IniConfig( "pypirc", data=dedent( """ [distutils] index-servers = pypi other [pypi] repository: username: password: [other] repository: http://example.com/pypi username: password: """ ), ) distutils, pypi, other = list(config) assert distutils["index-servers"] == "pypi\nother" assert pypi["repository"] == "" assert pypi["username"] == "" assert pypi["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)