fix iniparse
lint / docker (push) Successful in 9m14s

fix code passing ruff linter
pre-commit ruff
pre-commit ruff format
This commit is contained in:
2024-11-29 22:54:39 +01:00
parent aa8a68aa80
commit 737f9bea38
27 changed files with 2375 additions and 2016 deletions
+29 -13
View File
@@ -3,21 +3,37 @@
# Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk>
# All Rights Reserved. See LICENSE-PSF & LICENSE for details.
from .compat import ConfigParser, RawConfigParser, SafeConfigParser
from .config import BasicConfig, ConfigNamespace
from .configparser import (DEFAULTSECT, MAX_INTERPOLATION_DEPTH,
DuplicateSectionError, InterpolationDepthError,
InterpolationMissingOptionError,
InterpolationSyntaxError, NoOptionError,
NoSectionError)
from .ini import INIConfig, change_comment_syntax
from .config import BasicConfig, ConfigNamespace
from .compat import RawConfigParser, ConfigParser, SafeConfigParser
from .utils import tidy
from .configparser import (
DuplicateSectionError,
NoSectionError,
NoOptionError,
InterpolationMissingOptionError,
InterpolationDepthError,
InterpolationSyntaxError,
DEFAULTSECT,
MAX_INTERPOLATION_DEPTH,
)
__all__ = [
'BasicConfig', 'ConfigNamespace',
'INIConfig', 'tidy', 'change_comment_syntax',
'RawConfigParser', 'ConfigParser', 'SafeConfigParser',
'DuplicateSectionError', 'NoSectionError', 'NoOptionError',
'InterpolationMissingOptionError', 'InterpolationDepthError',
'InterpolationSyntaxError', 'DEFAULTSECT', 'MAX_INTERPOLATION_DEPTH',
"BasicConfig",
"ConfigNamespace",
"INIConfig",
"tidy",
"change_comment_syntax",
"RawConfigParser",
"ConfigParser",
"SafeConfigParser",
"DuplicateSectionError",
"NoSectionError",
"NoOptionError",
"InterpolationMissingOptionError",
"InterpolationDepthError",
"InterpolationSyntaxError",
"DEFAULTSECT",
"MAX_INTERPOLATION_DEPTH",
]
+90 -74
View File
@@ -12,41 +12,48 @@ The underlying INIConfig object can be accessed as cfg.data
"""
import re
from typing import Dict, List, TextIO, Optional, Type, Union, Tuple
import six
from .configparser import (
DuplicateSectionError,
NoSectionError,
NoOptionError,
InterpolationMissingOptionError,
InterpolationDepthError,
InterpolationSyntaxError,
DEFAULTSECT,
MAX_INTERPOLATION_DEPTH,
)
# These are imported only for compatibility.
# The code below does not reference them directly.
from .configparser import Error, InterpolationError, MissingSectionHeaderError, ParsingError
from . import ini
# These are imported only for compatiability.
# The code below does not reference them directly.
from .configparser import (DEFAULTSECT, MAX_INTERPOLATION_DEPTH,
DuplicateSectionError, Error,
InterpolationDepthError, InterpolationError,
InterpolationMissingOptionError,
InterpolationSyntaxError, MissingSectionHeaderError,
NoOptionError, NoSectionError, ParsingError)
class RawConfigParser(object):
def __init__(self, defaults=None, dict_type=dict):
if dict_type != dict:
raise ValueError('Custom dict types not supported')
class RawConfigParser:
def __init__(self, defaults: Optional[Dict[str, str]] = None, dict_type: Union[Type[Dict], str] = dict):
if not isinstance(dict_type, dict):
raise ValueError("Custom dict types not supported")
self.data = ini.INIConfig(defaults=defaults, optionxformsource=self)
def optionxform(self, optionstr):
def optionxform(self, optionstr: str) -> str:
return optionstr.lower()
def defaults(self):
d = {}
secobj = self.data._defaults
def defaults(self) -> Dict[str, str]:
d: Dict[str, str] = {}
secobj: ini.INISection = self.data._defaults
name: str
for name in secobj._options:
d[name] = secobj._compat_get(name)
return d
def sections(self):
def sections(self) -> List[str]:
"""Return a list of section names, excluding [DEFAULT]"""
return list(self.data)
def add_section(self, section):
def add_section(self, section: str) -> None:
"""Create a new section in the configuration.
Raise DuplicateSectionError if a section by the specified name
@@ -56,28 +63,28 @@ class RawConfigParser(object):
# The default section is the only one that gets the case-insensitive
# treatment - so it is special-cased here.
if section.lower() == "default":
raise ValueError('Invalid section name: %s' % section)
raise ValueError("Invalid section name: %s" % section)
if self.has_section(section):
raise DuplicateSectionError(section)
else:
self.data._new_namespace(section)
def has_section(self, section):
def has_section(self, section: str) -> bool:
"""Indicate whether the named section is present in the configuration.
The DEFAULT section is not acknowledged.
"""
return section in self.data
def options(self, section):
def options(self, section: str) -> List[str]:
"""Return a list of option names for the given section name."""
if section in self.data:
return list(self.data[section])
else:
raise NoSectionError(section)
def read(self, filenames):
def read(self, filenames: Union[List[str], str]) -> List[str]:
"""Read and parse a filename or a list of filenames.
Files that cannot be opened are silently ignored; this is
@@ -86,9 +93,11 @@ class RawConfigParser(object):
home directory, systemwide directory), and all existing
configuration files in the list will be read. A single
filename may also be given.
Returns the list of files that were read.
"""
files_read = []
if isinstance(filenames, six.string_types):
if isinstance(filenames, str):
filenames = [filenames]
for filename in filenames:
try:
@@ -100,7 +109,7 @@ class RawConfigParser(object):
fp.close()
return files_read
def readfp(self, fp, filename=None):
def readfp(self, fp: TextIO, filename: Optional[str] = None) -> None:
"""Like read() but the argument must be a file-like object.
The `fp' argument must have a `readline' method. Optional
@@ -110,60 +119,70 @@ class RawConfigParser(object):
"""
self.data._readfp(fp)
def get(self, section, option, vars=None):
def get(self, section: str, option: str, vars: dict = None) -> str:
if not self.has_section(section):
raise NoSectionError(section)
sec = self.data[section]
sec: ini.INISection = self.data[section]
if option in sec:
return sec._compat_get(option)
else:
raise NoOptionError(option, section)
def items(self, section):
def items(self, section: str) -> List[Tuple[str, str]]:
if section in self.data:
ans = []
opt: str
for opt in self.data[section]:
ans.append((opt, self.get(section, opt)))
return ans
else:
raise NoSectionError(section)
def getint(self, section, option):
def getint(self, section: str, option: str) -> int:
return int(self.get(section, option))
def getfloat(self, section, option):
def getfloat(self, section: str, option: str) -> float:
return float(self.get(section, option))
_boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False}
_boolean_states = {
"1": True,
"yes": True,
"true": True,
"on": True,
"0": False,
"no": False,
"false": False,
"off": False,
}
def getboolean(self, section, option):
def getboolean(self, section: str, option: str) -> bool:
v = self.get(section, option)
if v.lower() not in self._boolean_states:
raise ValueError('Not a boolean: %s' % v)
raise ValueError("Not a boolean: %s" % v)
return self._boolean_states[v.lower()]
def has_option(self, section, option):
def has_option(self, section: str, option: str) -> bool:
"""Check for the existence of a given option in a given section."""
if section in self.data:
sec = self.data[section]
else:
raise NoSectionError(section)
return (option in sec)
return option in sec
def set(self, section, option, value):
def set(self, section: str, option: str, value: str) -> None:
"""Set an option."""
if section in self.data:
self.data[section][option] = value
else:
raise NoSectionError(section)
def write(self, fp):
def write(self, fp: TextIO) -> None:
"""Write an .ini-format representation of the configuration state."""
fp.write(str(self.data))
def remove_option(self, section, option):
# FIXME Return a boolean instead of integer
def remove_option(self, section: str, option: str) -> int:
"""Remove an option."""
if section in self.data:
sec = self.data[section]
@@ -175,7 +194,7 @@ class RawConfigParser(object):
else:
return 0
def remove_section(self, section):
def remove_section(self, section: str) -> bool:
"""Remove a file section."""
if not self.has_section(section):
return False
@@ -183,15 +202,15 @@ class RawConfigParser(object):
return True
class ConfigDict(object):
"""Present a dict interface to a ini section."""
class ConfigDict:
"""Present a dict interface to an ini section."""
def __init__(self, cfg, section, vars):
self.cfg = cfg
self.section = section
self.vars = vars
def __init__(self, cfg: RawConfigParser, section: str, vars: dict):
self.cfg: RawConfigParser = cfg
self.section: str = section
self.vars: dict = vars
def __getitem__(self, key):
def __getitem__(self, key: str) -> Union[str, List[Union[int, str]]]:
try:
return RawConfigParser.get(self.cfg, self.section, key, self.vars)
except (NoOptionError, NoSectionError):
@@ -199,8 +218,13 @@ class ConfigDict(object):
class ConfigParser(RawConfigParser):
def get(self, section, option, raw=False, vars=None):
def get(
self,
section: str,
option: str,
raw: bool = False,
vars: Optional[dict] = None,
) -> object:
"""Get an option value for a given section.
All % interpolations are expanded in the return values, based on the
@@ -223,25 +247,24 @@ class ConfigParser(RawConfigParser):
d = ConfigDict(self, section, vars)
return self._interpolate(section, option, value, d)
def _interpolate(self, section, option, rawval, vars):
def _interpolate(self, section: str, option: str, rawval: object, vars: "ConfigDict"):
# do the string interpolation
value = rawval
depth = MAX_INTERPOLATION_DEPTH
while depth: # Loop through this until it's done
while depth: # Loop through this until it's done
depth -= 1
if "%(" in value:
try:
value = value % vars
except KeyError as e:
raise InterpolationMissingOptionError(
option, section, rawval, e.args[0])
raise InterpolationMissingOptionError(option, section, rawval, e.args[0])
else:
break
if value.find("%(") != -1:
raise InterpolationDepthError(option, section, rawval)
return value
def items(self, section, raw=False, vars=None):
def items(self, section: str, raw: bool = False, vars: Optional[dict] = None):
"""Return a list of tuples with (name, value) for each option
in the section.
@@ -269,40 +292,37 @@ class ConfigParser(RawConfigParser):
d = ConfigDict(self, section, vars)
if raw:
return [(option, d[option])
for option in options]
return [(option, d[option]) for option in options]
else:
return [(option, self._interpolate(section, option, d[option], d))
for option in options]
return [(option, self._interpolate(section, option, d[option], d)) for option in options]
class SafeConfigParser(ConfigParser):
_interpvar_re = re.compile(r"%\(([^)]+)\)s")
_badpercent_re = re.compile(r"%[^%]|%$")
def set(self, section, option, value):
if not isinstance(value, six.string_types):
def set(self, section: str, option: str, value: object) -> None:
if not isinstance(value, str):
raise TypeError("option values must be strings")
# check for bad percent signs:
# first, replace all "good" interpolations
tmp_value = self._interpvar_re.sub('', value)
tmp_value = self._interpvar_re.sub("", value)
# then, check if there's a lone percent sign left
m = self._badpercent_re.search(tmp_value)
if m:
raise ValueError("invalid interpolation syntax in %r at "
"position %d" % (value, m.start()))
raise ValueError("invalid interpolation syntax in %r at " "position %d" % (value, m.start()))
ConfigParser.set(self, section, option, value)
def _interpolate(self, section, option, rawval, vars):
def _interpolate(self, section: str, option: str, rawval: str, vars: ConfigDict):
# do the string interpolation
L = []
self._interpolate_some(option, L, rawval, section, vars, 1)
return ''.join(L)
return "".join(L)
_interpvar_match = re.compile(r"%\(([^)]+)\)s").match
def _interpolate_some(self, option, accum, rest, section, map, depth):
def _interpolate_some(self, option: str, accum: List[str], rest: str, section: str, map: ConfigDict, depth: int) -> None:
if depth > MAX_INTERPOLATION_DEPTH:
raise InterpolationDepthError(option, section, rest)
while rest:
@@ -323,18 +343,14 @@ class SafeConfigParser(ConfigParser):
if m is None:
raise InterpolationSyntaxError(option, section, "bad interpolation variable reference %r" % rest)
var = m.group(1)
rest = rest[m.end():]
rest = rest[m.end() :]
try:
v = map[var]
except KeyError:
raise InterpolationMissingOptionError(
option, section, rest, var)
raise InterpolationMissingOptionError(option, section, rest, var)
if "%" in v:
self._interpolate_some(option, accum, v,
section, map, depth + 1)
self._interpolate_some(option, accum, v, section, map, depth + 1)
else:
accum.append(v)
else:
raise InterpolationSyntaxError(
option, section,
"'%' must be followed by '%' or '(', found: " + repr(rest))
raise InterpolationSyntaxError(option, section, "'%' must be followed by '%' or '(', found: " + repr(rest))
+70 -53
View File
@@ -1,4 +1,10 @@
class ConfigNamespace(object):
from typing import Dict, Iterable, List, TextIO, Union, TYPE_CHECKING
if TYPE_CHECKING:
from .ini import INIConfig, INISection
class ConfigNamespace:
"""Abstract class representing the interface of Config objects.
A ConfigNamespace is a collection of names mapped to values, where
@@ -12,27 +18,27 @@ class ConfigNamespace(object):
Subclasses must implement the methods for container-like access,
and this class will automatically provide dotted access.
"""
# Methods that must be implemented by subclasses
def _getitem(self, key):
def _getitem(self, key: str) -> object:
return NotImplementedError(key)
def __setitem__(self, key, value):
def __setitem__(self, key: str, value: object):
raise NotImplementedError(key, value)
def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
raise NotImplementedError(key)
def __iter__(self):
def __iter__(self) -> Iterable[str]:
# FIXME Raise instead return
return NotImplementedError()
def _new_namespace(self, name):
def _new_namespace(self, name: str) -> "ConfigNamespace":
raise NotImplementedError(name)
def __contains__(self, key):
def __contains__(self, key: str) -> bool:
try:
self._getitem(key)
except KeyError:
@@ -44,35 +50,35 @@ class ConfigNamespace(object):
#
# To distinguish between accesses of class members and namespace
# keys, we first call object.__getattribute__(). If that succeeds,
# the name is assumed to be a class member. Otherwise it is
# the name is assumed to be a class member. Otherwise, it is
# treated as a namespace key.
#
# Therefore, member variables should be defined in the class,
# not just in the __init__() function. See BasicNamespace for
# an example.
def __getitem__(self, key):
def __getitem__(self, key: str) -> Union[object, "Undefined"]:
try:
return self._getitem(key)
except KeyError:
return Undefined(key, self)
def __getattr__(self, name):
def __getattr__(self, name: str) -> Union[object, "Undefined"]:
try:
return self._getitem(name)
except KeyError:
if name.startswith('__') and name.endswith('__'):
if name.startswith("__") and name.endswith("__"):
raise AttributeError
return Undefined(name, self)
def __setattr__(self, name, value):
def __setattr__(self, name: str, value: object) -> None:
try:
object.__getattribute__(self, name)
object.__setattr__(self, name, value)
except AttributeError:
self.__setitem__(name, value)
def __delattr__(self, name):
def __delattr__(self, name: str) -> None:
try:
object.__getattribute__(self, name)
object.__delattr__(self, name)
@@ -82,12 +88,12 @@ class ConfigNamespace(object):
# During unpickling, Python checks if the class has a __setstate__
# method. But, the data dicts have not been initialised yet, which
# leads to _getitem and hence __getattr__ raising an exception. So
# we explicitly impement default __setstate__ behavior.
def __setstate__(self, state):
# we explicitly implement default __setstate__ behavior.
def __setstate__(self, state: dict) -> None:
self.__dict__.update(state)
class Undefined(object):
class Undefined:
"""Helper class used to hold undefined names until assignment.
This class helps create any undefined subsections when an
@@ -95,21 +101,24 @@ class Undefined(object):
statement is "cfg.a.b.c = 42", but "cfg.a.b" does not exist yet.
"""
def __init__(self, name, namespace):
object.__setattr__(self, 'name', name)
object.__setattr__(self, 'namespace', namespace)
def __init__(self, name: str, namespace: ConfigNamespace):
# FIXME These assignments into `object` feel very strange.
# What's the reason for it?
object.__setattr__(self, "name", name)
object.__setattr__(self, "namespace", namespace)
def __setattr__(self, name, value):
def __setattr__(self, name: str, value: object) -> None:
obj = self.namespace._new_namespace(self.name)
obj[name] = value
def __setitem__(self, name, value):
def __setitem__(self, name, value) -> None:
obj = self.namespace._new_namespace(self.name)
obj[name] = value
# ---- Basic implementation of a ConfigNamespace
class BasicConfig(ConfigNamespace):
"""Represents a hierarchical collection of named values.
@@ -161,7 +170,7 @@ class BasicConfig(ConfigNamespace):
Finally, values can be read from a file as follows:
>>> from six import StringIO
>>> from io import StringIO
>>> sio = StringIO('''
... # comment
... ui.height = 100
@@ -181,66 +190,73 @@ class BasicConfig(ConfigNamespace):
"""
# this makes sure that __setattr__ knows this is not a namespace key
_data = None
_data: Dict[str, str] = None
def __init__(self):
self._data = {}
def _getitem(self, key):
def _getitem(self, key: str) -> str:
return self._data[key]
def __setitem__(self, key, value):
def __setitem__(self, key: str, value: object) -> None:
# FIXME We can add any object as 'value', but when an integer is read
# from a file, it will be a string. Should we explicitly convert
# this 'value' to string, to ensure consistency?
# It will stay the original type until it is written to a file.
self._data[key] = value
def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
del self._data[key]
def __iter__(self):
def __iter__(self) -> Iterable[str]:
return iter(self._data)
def __str__(self, prefix=''):
lines = []
keys = list(self._data.keys())
def __str__(self, prefix: str = "") -> str:
lines: List[str] = []
keys: List[str] = list(self._data.keys())
keys.sort()
for name in keys:
value = self._data[name]
value: object = self._data[name]
if isinstance(value, ConfigNamespace):
lines.append(value.__str__(prefix='%s%s.' % (prefix,name)))
lines.append(value.__str__(prefix="%s%s." % (prefix, name)))
else:
if value is None:
lines.append('%s%s' % (prefix, name))
lines.append("%s%s" % (prefix, name))
else:
lines.append('%s%s = %s' % (prefix, name, value))
return '\n'.join(lines)
lines.append("%s%s = %s" % (prefix, name, value))
return "\n".join(lines)
def _new_namespace(self, name):
def _new_namespace(self, name: str) -> "BasicConfig":
obj = BasicConfig()
self._data[name] = obj
return obj
def _readfp(self, fp):
def _readfp(self, fp: TextIO) -> None:
while True:
line = fp.readline()
line: str = fp.readline()
if not line:
break
line = line.strip()
if not line: continue
if line[0] == '#': continue
data = line.split('=', 1)
if not line:
continue
if line[0] == "#":
continue
data: List[str] = line.split("=", 1)
if len(data) == 1:
name = line
value = None
else:
name = data[0].strip()
value = data[1].strip()
name_components = name.split('.')
ns = self
name_components = name.split(".")
ns: ConfigNamespace = self
for n in name_components[:-1]:
if n in ns:
ns = ns[n]
if not isinstance(ns, ConfigNamespace):
raise TypeError('value-namespace conflict', n)
maybe_ns: object = ns[n]
if not isinstance(maybe_ns, ConfigNamespace):
raise TypeError("value-namespace conflict", n)
ns = maybe_ns
else:
ns = ns._new_namespace(n)
ns[name_components[-1]] = value
@@ -248,7 +264,8 @@ class BasicConfig(ConfigNamespace):
# ---- Utility functions
def update_config(target, source):
def update_config(target: ConfigNamespace, source: ConfigNamespace):
"""Imports values from source into target.
Recursively walks the <source> ConfigNamespace and inserts values
@@ -276,15 +293,15 @@ def update_config(target, source):
display_clock = True
display_qlength = True
width = 150
"""
for name in sorted(source):
value = source[name]
value: object = source[name]
if isinstance(value, ConfigNamespace):
if name in target:
myns = target[name]
if not isinstance(myns, ConfigNamespace):
raise TypeError('value-namespace conflict')
maybe_myns: object = target[name]
if not isinstance(maybe_myns, ConfigNamespace):
raise TypeError("value-namespace conflict")
myns = maybe_myns
else:
myns = target._new_namespace(name)
update_config(myns, value)
+2 -7
View File
@@ -1,7 +1,2 @@
try:
# not all objects get imported with __all__
from ConfigParser import *
from ConfigParser import Error, InterpolationMissingOptionError
except ImportError:
from configparser import *
from configparser import Error, InterpolationMissingOptionError
from configparser import *
from configparser import Error, InterpolationMissingOptionError
+236 -202
View File
@@ -7,7 +7,7 @@
Example:
>>> from six import StringIO
>>> from io import StringIO
>>> sio = StringIO('''# configure foo-application
... [foo]
... bar1 = qualia
@@ -39,26 +39,31 @@ Example:
# An ini parser that supports ordered sections/options
# Also supports updates, while preserving structure
# Backward-compatiable with ConfigParser
# Backward-compatible with ConfigParser
import re
import six
from typing import Any, Callable, Dict, TextIO, Iterator, List, Optional, Set, Union
from typing import TYPE_CHECKING
from .configparser import DEFAULTSECT, ParsingError, MissingSectionHeaderError
from . import config
from .configparser import DEFAULTSECT, MissingSectionHeaderError, ParsingError
if TYPE_CHECKING:
from compat import RawConfigParser
class LineType(object):
line = None
class LineType:
line: Optional[str] = None
def __init__(self, line=None):
def __init__(self, line: Optional[str] = None) -> None:
if line is not None:
self.line = line.strip('\n')
self.line = line.strip("\n")
# Return the original line for unmodified objects
# Otherwise construct using the current attribute values
def __str__(self):
def __str__(self) -> str:
if self.line is not None:
return self.line
else:
@@ -66,78 +71,87 @@ class LineType(object):
# If an attribute is modified after initialization
# set line to None since it is no longer accurate.
def __setattr__(self, name, value):
if hasattr(self,name):
self.__dict__['line'] = None
def __setattr__(self, name: str, value: object) -> None:
if hasattr(self, name):
self.__dict__["line"] = None
self.__dict__[name] = value
def to_string(self):
raise Exception('This method must be overridden in derived classes')
def to_string(self) -> str:
# FIXME Raise NotImplementedError instead
raise Exception("This method must be overridden in derived classes")
class SectionLine(LineType):
regex = re.compile(r'^\['
r'(?P<name>[^]]+)'
r'\]\s*'
r'((?P<csep>;|#)(?P<comment>.*))?$')
regex = re.compile(r"^\[" r"(?P<name>[^]]+)" r"\]\s*" r"((?P<csep>;|#)(?P<comment>.*))?$")
def __init__(self, name, comment=None, comment_separator=None,
comment_offset=-1, line=None):
super(SectionLine, self).__init__(line)
self.name = name
self.comment = comment
self.comment_separator = comment_separator
self.comment_offset = comment_offset
def __init__(
self,
name: str,
comment: Optional[str] = None,
comment_separator: Optional[str] = None,
comment_offset: int = -1,
line: Optional[str] = None,
) -> None:
super().__init__(line)
self.name: str = name
self.comment: Optional[str] = comment
self.comment_separator: Optional[str] = comment_separator
self.comment_offset: int = comment_offset
def to_string(self):
out = '[' + self.name + ']'
def to_string(self) -> str:
out: str = "[" + self.name + "]"
if self.comment is not None:
# try to preserve indentation of comments
out = (out+' ').ljust(self.comment_offset)
out = (out + " ").ljust(self.comment_offset)
out = out + self.comment_separator + self.comment
return out
def parse(cls, line):
m = cls.regex.match(line.rstrip())
@classmethod
def parse(cls, line: str) -> Optional["SectionLine"]:
m: Optional[re.Match] = cls.regex.match(line.rstrip())
if m is None:
return None
return cls(m.group('name'), m.group('comment'),
m.group('csep'), m.start('csep'),
line)
parse = classmethod(parse)
return cls(m.group("name"), m.group("comment"), m.group("csep"), m.start("csep"), line)
class OptionLine(LineType):
def __init__(self, name, value, separator=' = ', comment=None,
comment_separator=None, comment_offset=-1, line=None):
super(OptionLine, self).__init__(line)
self.name = name
self.value = value
self.separator = separator
self.comment = comment
self.comment_separator = comment_separator
self.comment_offset = comment_offset
def __init__(
self,
name: str,
value: object,
separator: str = " = ",
comment: Optional[str] = None,
comment_separator: Optional[str] = None,
comment_offset: int = -1,
line: Optional[str] = None,
) -> None:
super().__init__(line)
self.name: str = name
self.value: object = value
self.separator: str = separator
self.comment: Optional[str] = comment
self.comment_separator: Optional[str] = comment_separator
self.comment_offset: int = comment_offset
def to_string(self):
out = '%s%s%s' % (self.name, self.separator, self.value)
def to_string(self) -> str:
out: str = "%s%s%s" % (self.name, self.separator, self.value)
if self.comment is not None:
# try to preserve indentation of comments
out = (out+' ').ljust(self.comment_offset)
out = (out + " ").ljust(self.comment_offset)
out = out + self.comment_separator + self.comment
return out
regex = re.compile(r'^(?P<name>[^:=\s[][^:=]*)'
r'(?P<sep>[:=]\s*)'
r'(?P<value>.*)$')
regex = re.compile(r"^(?P<name>[^:=\s[][^:=]*)" r"(?P<sep>[:=]\s*)" r"(?P<value>.*)$")
def parse(cls, line):
m = cls.regex.match(line.rstrip())
@classmethod
def parse(cls, line: str) -> Optional["OptionLine"]:
m: Optional[re.Match] = cls.regex.match(line.rstrip())
if m is None:
return None
name = m.group('name').rstrip()
value = m.group('value')
sep = m.group('name')[len(name):] + m.group('sep')
name: str = m.group("name").rstrip()
value: str = m.group("value")
sep: str = m.group("name")[len(name) :] + m.group("sep")
# comments are not detected in the regex because
# ensuring total compatibility with ConfigParser
@@ -150,123 +164,120 @@ class OptionLine(LineType):
# include ';' in the value needs to be addressed.
# Also, '#' doesn't mark comments in options...
coff = value.find(';')
if coff != -1 and value[coff-1].isspace():
comment = value[coff+1:]
coff: int = value.find(";")
if coff != -1 and value[coff - 1].isspace():
comment = value[coff + 1 :]
csep = value[coff]
value = value[:coff].rstrip()
coff = m.start('value') + coff
coff = m.start("value") + coff
else:
comment = None
csep = None
coff = -1
return cls(name, value, sep, comment, csep, coff, line)
parse = classmethod(parse)
def change_comment_syntax(comment_chars='%;#', allow_rem=False):
comment_chars = re.sub(r'([\]\-\^])', r'\\\1', comment_chars)
regex = r'^(?P<csep>[%s]' % comment_chars
def change_comment_syntax(comment_chars: str = "%;#", allow_rem: bool = False) -> None:
comment_chars: str = re.sub(r"([\]\-\^])", r"\\\1", comment_chars)
regex: str = r"^(?P<csep>[%s]" % comment_chars
if allow_rem:
regex += '|[rR][eE][mM]'
regex += r')(?P<comment>.*)$'
regex += "|[rR][eE][mM]"
regex += r")(?P<comment>.*)$"
CommentLine.regex = re.compile(regex)
class CommentLine(LineType):
regex = re.compile(r'^(?P<csep>[;#])'
r'(?P<comment>.*)$')
regex: re.Pattern = re.compile(r"^(?P<csep>[;#]|[rR][eE][mM])" r"(?P<comment>.*)$")
def __init__(self, comment='', separator='#', line=None):
super(CommentLine, self).__init__(line)
self.comment = comment
self.separator = separator
def __init__(self, comment: str = "", separator: str = "#", line: Optional[str] = None) -> None:
super().__init__(line)
self.comment: str = comment
self.separator: str = separator
def to_string(self):
def to_string(self) -> str:
return self.separator + self.comment
def parse(cls, line):
m = cls.regex.match(line.rstrip())
@classmethod
def parse(cls, line: str) -> Optional["CommentLine"]:
m: Optional[re.Match] = cls.regex.match(line.rstrip())
if m is None:
return None
return cls(m.group('comment'), m.group('csep'), line)
parse = classmethod(parse)
return cls(m.group("comment"), m.group("csep"), line)
class EmptyLine(LineType):
# could make this a singleton
def to_string(self):
return ''
def to_string(self) -> str:
return ""
value = property(lambda self: '')
value = property(lambda self: "")
def parse(cls, line):
@classmethod
def parse(cls, line: str) -> Optional["EmptyLine"]:
if line.strip():
return None
return cls(line)
parse = classmethod(parse)
class ContinuationLine(LineType):
regex = re.compile(r'^\s+(?P<value>.*)$')
regex: re.Pattern = re.compile(r"^\s+(?P<value>.*)$")
def __init__(self, value, value_offset=None, line=None):
super(ContinuationLine, self).__init__(line)
def __init__(self, value: str, value_offset: Optional[int] = None, line: Optional[str] = None) -> None:
super().__init__(line)
self.value = value
if value_offset is None:
value_offset = 8
self.value_offset = value_offset
self.value_offset: int = value_offset
def to_string(self):
return ' '*self.value_offset + self.value
def to_string(self) -> str:
return " " * self.value_offset + self.value
def parse(cls, line):
m = cls.regex.match(line.rstrip())
@classmethod
def parse(cls, line: str) -> Optional["ContinuationLine"]:
m: Optional[re.Match] = cls.regex.match(line.rstrip())
if m is None:
return None
return cls(m.group('value'), m.start('value'), line)
parse = classmethod(parse)
return cls(m.group("value"), m.start("value"), line)
class LineContainer(object):
def __init__(self, d=None):
class LineContainer:
def __init__(self, d: Optional[Union[List[LineType], LineType]] = None) -> None:
self.contents = []
self.orgvalue = None
self.orgvalue: str = None
if d:
if isinstance(d, list): self.extend(d)
else: self.add(d)
if isinstance(d, list):
self.extend(d)
else:
self.add(d)
def add(self, x):
def add(self, x: LineType) -> None:
self.contents.append(x)
def extend(self, x):
for i in x: self.add(i)
def extend(self, x: List[LineType]) -> None:
for i in x:
self.add(i)
def get_name(self):
def get_name(self) -> str:
return self.contents[0].name
def set_name(self, data):
def set_name(self, data: str) -> None:
self.contents[0].name = data
def get_value(self):
def get_value(self) -> str:
if self.orgvalue is not None:
return self.orgvalue
elif len(self.contents) == 1:
return self.contents[0].value
else:
return '\n'.join([('%s' % x.value) for x in self.contents
if not isinstance(x, CommentLine)])
return "\n".join([("%s" % x.value) for x in self.contents if not isinstance(x, CommentLine)])
def set_value(self, data):
def set_value(self, data: object) -> None:
self.orgvalue = data
lines = ('%s' % data).split('\n')
lines: List[str] = ("%s" % data).split("\n")
# If there is an existing ContinuationLine, use its offset
value_offset = None
value_offset: Optional[int] = None
for v in self.contents:
if isinstance(v, ContinuationLine):
value_offset = v.value_offset
@@ -282,40 +293,45 @@ class LineContainer(object):
else:
self.add(EmptyLine())
def get_line_number(self) -> Optional[int]:
return self.contents[0].line_number if self.contents else None
name = property(get_name, set_name)
value = property(get_value, set_value)
def __str__(self):
s = [x.__str__() for x in self.contents]
return '\n'.join(s)
line_number = property(get_line_number)
def finditer(self, key):
def __str__(self) -> str:
s: List[str] = [x.__str__() for x in self.contents]
return "\n".join(s)
def finditer(self, key: str) -> Iterator[Union[SectionLine, OptionLine]]:
for x in self.contents[::-1]:
if hasattr(x, 'name') and x.name==key:
if hasattr(x, "name") and x.name == key:
yield x
def find(self, key):
def find(self, key: str) -> Union[SectionLine, OptionLine]:
for x in self.finditer(key):
return x
raise KeyError(key)
def _make_xform_property(myattrname, srcattrname=None):
private_attrname = myattrname + 'value'
private_srcname = myattrname + 'source'
def _make_xform_property(myattrname: str, srcattrname: Optional[str] = None) -> property:
private_attrname: str = myattrname + "value"
private_srcname: str = myattrname + "source"
if srcattrname is None:
srcattrname = myattrname
def getfn(self):
srcobj = getattr(self, private_srcname)
def getfn(self) -> Callable:
srcobj: Optional[object] = getattr(self, private_srcname)
if srcobj is not None:
return getattr(srcobj, srcattrname)
else:
return getattr(self, private_attrname)
def setfn(self, value):
srcobj = getattr(self, private_srcname)
def setfn(self, value: Callable) -> None:
srcobj: Optional[object] = getattr(self, private_srcname)
if srcobj is not None:
setattr(srcobj, srcattrname, value)
else:
@@ -325,31 +341,38 @@ def _make_xform_property(myattrname, srcattrname=None):
class INISection(config.ConfigNamespace):
_lines = None
_options = None
_defaults = None
_optionxformvalue = None
_optionxformsource = None
_compat_skip_empty_lines = set()
_lines: List[LineContainer] = None
_options: Dict[str, object] = None
_defaults: Optional["INISection"] = None
_optionxformvalue: "INIConfig" = None
_optionxformsource: "INIConfig" = None
_compat_skip_empty_lines: Set[str] = set()
def __init__(self, lineobj, defaults=None, optionxformvalue=None, optionxformsource=None):
def __init__(
self,
lineobj: LineContainer,
defaults: Optional["INISection"] = None,
optionxformvalue: Optional["INIConfig"] = None,
optionxformsource: Optional["INIConfig"] = None,
) -> None:
self._lines = [lineobj]
self._defaults = defaults
self._optionxformvalue = optionxformvalue
self._optionxformsource = optionxformsource
self._options = {}
_optionxform = _make_xform_property('_optionxform')
_optionxform = _make_xform_property("_optionxform")
def _compat_get(self, key):
def _compat_get(self, key: str) -> str:
# identical to __getitem__ except that _compat_XXX
# is checked for backward-compatible handling
if key == '__name__':
if key == "__name__":
return self._lines[-1].name
if self._optionxform: key = self._optionxform(key)
if self._optionxform:
key = self._optionxform(key)
try:
value = self._options[key].value
del_empty = key in self._compat_skip_empty_lines
value: str = self._options[key].value
del_empty: bool = key in self._compat_skip_empty_lines
except KeyError:
if self._defaults and key in self._defaults._options:
value = self._defaults._options[key].value
@@ -357,13 +380,14 @@ class INISection(config.ConfigNamespace):
else:
raise
if del_empty:
value = re.sub('\n+', '\n', value)
value = re.sub("\n+", "\n", value)
return value
def _getitem(self, key):
if key == '__name__':
def _getitem(self, key: str) -> object:
if key == "__name__":
return self._lines[-1].name
if self._optionxform: key = self._optionxform(key)
if self._optionxform:
key = self._optionxform(key)
try:
return self._options[key].value
except KeyError:
@@ -372,22 +396,25 @@ class INISection(config.ConfigNamespace):
else:
raise
def __setitem__(self, key, value):
if self._optionxform: xkey = self._optionxform(key)
else: xkey = key
def __setitem__(self, key: str, value: object) -> None:
if self._optionxform:
xkey = self._optionxform(key)
else:
xkey = key
if xkey in self._compat_skip_empty_lines:
self._compat_skip_empty_lines.remove(xkey)
if xkey not in self._options:
# create a dummy object - value may have multiple lines
obj = LineContainer(OptionLine(key, ''))
obj = LineContainer(OptionLine(key, ""))
self._lines[-1].add(obj)
self._options[xkey] = obj
# the set_value() function in LineContainer
# automatically handles multi-line values
self._options[xkey].value = value
def __delitem__(self, key):
if self._optionxform: key = self._optionxform(key)
def __delitem__(self, key: str) -> None:
if self._optionxform:
key = self._optionxform(key)
if key in self._compat_skip_empty_lines:
self._compat_skip_empty_lines.remove(key)
for l in self._lines:
@@ -395,14 +422,16 @@ class INISection(config.ConfigNamespace):
for o in l.contents:
if isinstance(o, LineContainer):
n = o.name
if self._optionxform: n = self._optionxform(n)
if key != n: remaining.append(o)
if self._optionxform:
n = self._optionxform(n)
if key != n:
remaining.append(o)
else:
remaining.append(o)
l.contents = remaining
del self._options[key]
def __iter__(self):
def __iter__(self) -> Iterator[str]:
d = set()
for l in self._lines:
for x in l.contents:
@@ -421,26 +450,25 @@ class INISection(config.ConfigNamespace):
d.add(x)
def _new_namespace(self, name):
raise Exception('No sub-sections allowed', name)
raise Exception("No sub-sections allowed", name)
def make_comment(line):
return CommentLine(line.rstrip('\n'))
def make_comment(line: str) -> CommentLine:
return CommentLine(line.rstrip("\n"))
def readline_iterator(f):
"""iterate over a file by only using the file object's readline method"""
have_newline = False
def readline_iterator(f: TextIO) -> Iterator[str]:
"""Iterate over a file by only using the file object's readline method."""
have_newline: bool = False
while True:
line = f.readline()
line: Optional[str] = f.readline()
if not line:
if have_newline:
yield ""
return
if line.endswith('\n'):
if line.endswith("\n"):
have_newline = True
else:
have_newline = False
@@ -448,57 +476,67 @@ def readline_iterator(f):
yield line
def lower(x):
def lower(x: str) -> str:
return x.lower()
class INIConfig(config.ConfigNamespace):
_data = None
_sections = None
_defaults = None
_optionxformvalue = None
_optionxformsource = None
_sectionxformvalue = None
_sectionxformsource = None
_data: LineContainer = None
_sections: Dict[str, object] = None
_defaults: INISection = None
_optionxformvalue: Callable = None
_optionxformsource: Optional["INIConfig"] = None
_sectionxformvalue: Optional["INIConfig"] = None
_sectionxformsource: Optional["INIConfig"] = None
_parse_exc = None
_bom = False
def __init__(self, fp=None, defaults=None, parse_exc=True,
optionxformvalue=lower, optionxformsource=None,
sectionxformvalue=None, sectionxformsource=None):
def __init__(
self,
fp: TextIO = None,
defaults: Dict[str, object] = None,
parse_exc: bool = True,
optionxformvalue: Callable = lower,
optionxformsource: Optional[Union["INIConfig", "RawConfigParser"]] = None,
sectionxformvalue: Optional["INIConfig"] = None,
sectionxformsource: Optional["INIConfig"] = None,
) -> None:
self._data = LineContainer()
self._parse_exc = parse_exc
self._optionxformvalue = optionxformvalue
self._optionxformsource = optionxformsource
self._sectionxformvalue = sectionxformvalue
self._sectionxformsource = sectionxformsource
self._sections = {}
if defaults is None: defaults = {}
self._sections: Dict[str, INISection] = {}
if defaults is None:
defaults = {}
self._defaults = INISection(LineContainer(), optionxformsource=self)
for name, value in defaults.items():
self._defaults[name] = value
if fp is not None:
self._readfp(fp)
_optionxform = _make_xform_property('_optionxform', 'optionxform')
_sectionxform = _make_xform_property('_sectionxform', 'optionxform')
_optionxform = _make_xform_property("_optionxform", "optionxform")
_sectionxform = _make_xform_property("_sectionxform", "optionxform")
def _getitem(self, key):
def _getitem(self, key: str) -> INISection:
if key == DEFAULTSECT:
return self._defaults
if self._sectionxform: key = self._sectionxform(key)
if self._sectionxform:
key = self._sectionxform(key)
return self._sections[key]
def __setitem__(self, key, value):
raise Exception('Values must be inside sections', key, value)
def __setitem__(self, key: str, value: object):
raise Exception("Values must be inside sections", key, value)
def __delitem__(self, key):
if self._sectionxform: key = self._sectionxform(key)
def __delitem__(self, key: str) -> None:
if self._sectionxform:
key = self._sectionxform(key)
for line in self._sections[key]._lines:
self._data.contents.remove(line)
del self._sections[key]
def __iter__(self):
def __iter__(self) -> Iterator[str]:
d = set()
d.add(DEFAULTSECT)
for x in self._data.contents:
@@ -507,35 +545,31 @@ class INIConfig(config.ConfigNamespace):
yield x.name
d.add(x.name)
def _new_namespace(self, name):
def _new_namespace(self, name: str) -> INISection:
if self._data.contents:
self._data.add(EmptyLine())
obj = LineContainer(SectionLine(name))
self._data.add(obj)
if self._sectionxform: name = self._sectionxform(name)
if self._sectionxform:
name = self._sectionxform(name)
if name in self._sections:
ns = self._sections[name]
ns._lines.append(obj)
else:
ns = INISection(obj, defaults=self._defaults,
optionxformsource=self)
ns = INISection(obj, defaults=self._defaults, optionxformsource=self)
self._sections[name] = ns
return ns
def __str__(self):
def __str__(self) -> str:
if self._bom:
fmt = u'\ufeff%s'
fmt = "\ufeff%s"
else:
fmt = '%s'
fmt = "%s"
return fmt % self._data.__str__()
__unicode__ = __str__
_line_types = [EmptyLine, CommentLine, SectionLine, OptionLine, ContinuationLine]
_line_types = [EmptyLine, CommentLine,
SectionLine, OptionLine,
ContinuationLine]
def _parse(self, line):
def _parse(self, line: str) -> Any:
for linetype in self._line_types:
lineobj = linetype.parse(line)
if lineobj:
@@ -544,7 +578,7 @@ class INIConfig(config.ConfigNamespace):
# can't parse line
return None
def _readfp(self, fp):
def _readfp(self, fp: TextIO) -> None:
cur_section = None
cur_option = None
cur_section_name = None
@@ -554,21 +588,20 @@ class INIConfig(config.ConfigNamespace):
try:
fname = fp.name
except AttributeError:
fname = '<???>'
fname = "<???>"
line_count = 0
exc = None
line = None
for line in readline_iterator(fp):
# Check for BOM on first line
if line_count == 0 and isinstance(line, six.text_type):
if line[0] == u'\ufeff':
if line_count == 0 and isinstance(line, str):
if line[0] == "\ufeff":
line = line[1:]
self._bom = True
line_obj = self._parse(line)
line_count += 1
if not cur_section and not isinstance(line_obj, (CommentLine, EmptyLine, SectionLine)):
if self._parse_exc:
raise MissingSectionHeaderError(fname, line_count, line)
@@ -588,7 +621,7 @@ class INIConfig(config.ConfigNamespace):
cur_option.extend(pending_lines)
pending_lines = []
if pending_empty_lines:
optobj._compat_skip_empty_lines.add(cur_option_name)
optobj._compat_skip_empty_lines.add(cur_option_name) # noqa : F821
pending_empty_lines = False
cur_option.add(line_obj)
else:
@@ -633,9 +666,7 @@ class INIConfig(config.ConfigNamespace):
else:
cur_section_name = cur_section.name
if cur_section_name not in self._sections:
self._sections[cur_section_name] = \
INISection(cur_section, defaults=self._defaults,
optionxformsource=self)
self._sections[cur_section_name] = INISection(cur_section, defaults=self._defaults, optionxformsource=self)
else:
self._sections[cur_section_name]._lines.append(cur_section)
@@ -644,8 +675,11 @@ class INIConfig(config.ConfigNamespace):
if isinstance(line_obj, EmptyLine):
pending_empty_lines = True
if line_obj:
line_obj.line_number = line_count
self._data.extend(pending_lines)
if line and line[-1] == '\n':
if line and line[-1] == "\n":
self._data.add(EmptyLine())
if exc:
+11 -7
View File
@@ -1,8 +1,13 @@
from typing import TYPE_CHECKING, List
from . import compat
from .ini import EmptyLine, LineContainer
if TYPE_CHECKING:
from .ini import LineType
def tidy(cfg):
def tidy(cfg: compat.RawConfigParser):
"""Clean up blank lines.
This functions makes the configuration look clean and
@@ -19,8 +24,7 @@ def tidy(cfg):
if isinstance(cont[i], LineContainer):
tidy_section(cont[i])
i += 1
elif (isinstance(cont[i-1], EmptyLine) and
isinstance(cont[i], EmptyLine)):
elif isinstance(cont[i - 1], EmptyLine) and isinstance(cont[i], EmptyLine):
del cont[i]
else:
i += 1
@@ -34,11 +38,11 @@ def tidy(cfg):
cont.append(EmptyLine())
def tidy_section(lc):
cont = lc.contents
i = 1
def tidy_section(lc: "LineContainer"):
cont: List[LineType] = lc.contents
i: int = 1
while i < len(cont):
if isinstance(cont[i-1], EmptyLine) and isinstance(cont[i], EmptyLine):
if isinstance(cont[i - 1], EmptyLine) and isinstance(cont[i], EmptyLine):
del cont[i]
else:
i += 1