diff --git a/pyhocon/config_parser.py b/pyhocon/config_parser.py index 4936f1e..de0ee99 100644 --- a/pyhocon/config_parser.py +++ b/pyhocon/config_parser.py @@ -12,7 +12,7 @@ ParserElement, ParseSyntaxException, QuotedString, Regex, SkipTo, StringEnd, Suppress, TokenConverter, Word, ZeroOrMore, alphanums, alphas8bit, col, lineno, - replaceWith) + replace_with) from pyhocon.period_parser import get_period_expr @@ -35,53 +35,20 @@ def fixed_get_attr(self, item): from pyhocon.exceptions import (ConfigException, ConfigMissingException, ConfigSubstitutionException) -use_urllib2 = False -try: - # For Python 3.0 and later - from urllib.request import urlopen - from urllib.error import HTTPError, URLError -except ImportError: # pragma: no cover - # Fall back to Python 2's urllib2 - from urllib2 import urlopen, HTTPError, URLError - - use_urllib2 = True -try: - basestring -except NameError: # pragma: no cover - basestring = str - unicode = str - -if sys.version_info < (3, 5): - def glob(pathname, recursive=False): - if recursive and '**' in pathname: - import warnings - warnings.warn('This version of python (%s) does not support recursive import' % sys.version) - from glob import glob as _glob - return _glob(pathname) -else: - from glob import glob - -# Fix deprecated warning with 'imp' library and Python 3.4+. -# See: https://github.com/chimpler/pyhocon/issues/248 -if sys.version_info >= (3, 4): - import importlib.util - - - def find_package_dirs(name): - spec = importlib.util.find_spec(name) - # When `imp.find_module()` cannot find a package it raises ImportError. - # Here we should simulate it to keep the compatibility with older - # versions. - if not spec: - raise ImportError('No module named {!r}'.format(name)) - return spec.submodule_search_locations -else: - import imp - import importlib - - - def find_package_dirs(name): - return [imp.find_module(name)[1]] +from urllib.request import urlopen +from urllib.error import HTTPError, URLError +from glob import glob +import importlib.util + + +def find_package_dirs(name): + spec = importlib.util.find_spec(name) + # When `imp.find_module()` cannot find a package it raises ImportError. + # Here we should simulate it to keep the compatibility with older + # versions. + if not spec: + raise ImportError('No module named {!r}'.format(name)) + return spec.submodule_search_locations logger = logging.getLogger(__name__) @@ -107,11 +74,8 @@ class STR_SUBSTITUTION(object): pass -U_KEY_SEP = unicode('.') -U_KEY_FMT = unicode('"{0}"') - -U_KEY_SEP = unicode('.') -U_KEY_FMT = unicode('"{0}"') +U_KEY_SEP = '.' +U_KEY_FMT = '"{0}"' class ConfigFactory(object): @@ -121,9 +85,9 @@ def parse_file(cls, filename, encoding='utf-8', required=True, resolve=True, unr """Parse file :param filename: filename - :type filename: basestring + :type filename: str :param encoding: file encoding - :type encoding: basestring + :type encoding: str :param required: If true, raises an exception if can't load file :type required: boolean :param resolve: if true, resolve substitutions @@ -150,7 +114,7 @@ def parse_URL(cls, url, timeout=None, resolve=True, required=False, unresolved_v """Parse URL :param url: url to parse - :type url: basestring + :type url: str :param resolve: if true, resolve substitutions :type resolve: boolean :param unresolved_value: assigned value to unresolved substitution. @@ -164,7 +128,7 @@ def parse_URL(cls, url, timeout=None, resolve=True, required=False, unresolved_v try: with contextlib.closing(urlopen(url, timeout=socket_timeout)) as fd: - content = fd.read() if use_urllib2 else fd.read().decode('utf-8') + content = fd.read().decode('utf-8') return cls.parse_string(content, os.path.dirname(url), resolve, unresolved_value) except (HTTPError, URLError) as e: logger.warn('Cannot include url %s. Resource is inaccessible.', url) @@ -178,7 +142,7 @@ def parse_string(cls, content, basedir=None, resolve=True, unresolved_value=DEFA """Parse string :param content: content to parse - :type content: basestring + :type content: str :param resolve: if true, resolve substitutions :type resolve: boolean :param unresolved_value: assigned value to unresolved substitution. @@ -235,7 +199,7 @@ def parse(cls, content, basedir=None, resolve=True, unresolved_value=DEFAULT_SUB """parse a HOCON content :param content: HOCON content to parse - :type content: basestring + :type content: str :param resolve: if true, resolve substitutions :type resolve: boolean :param unresolved_value: assigned value to unresolved substitution. @@ -376,17 +340,17 @@ def _merge(a, b): @contextlib.contextmanager def set_default_white_spaces(): default = ParserElement.DEFAULT_WHITE_CHARS - ParserElement.setDefaultWhitespaceChars(' \t') + ParserElement.set_default_whitespace_chars(' \t') yield - ParserElement.setDefaultWhitespaceChars(default) + ParserElement.set_default_whitespace_chars(default) with set_default_white_spaces(): assign_expr = Forward() - true_expr = Keyword("true", caseless=True).setParseAction(replaceWith(True)) - false_expr = Keyword("false", caseless=True).setParseAction(replaceWith(False)) - null_expr = Keyword("null", caseless=True).setParseAction(replaceWith(NoneValue())) - key = QuotedString('"""', escChar='\\', unquoteResults=False) | \ - QuotedString('"', escChar='\\', unquoteResults=False) | Word(alphanums + alphas8bit + '._- /') + true_expr = Keyword("true", case_insensitive=True).set_parse_action(replace_with(True)) + false_expr = Keyword("false", case_insensitive=True).set_parse_action(replace_with(False)) + null_expr = Keyword("null", case_insensitive=True).set_parse_action(replace_with(NoneValue())) + key = QuotedString('"""', esc_char='\\', unquote_results=False) | \ + QuotedString('"', esc_char='\\', unquote_results=False) | Word(alphanums + alphas8bit + '._- /') eol = Word('\n\r').suppress() eol_comma = Word('\n\r,').suppress() @@ -394,20 +358,20 @@ def set_default_white_spaces(): comment_eol = Suppress(Optional(eol_comma) + comment) comment_no_comma_eol = (comment | eol).suppress() number_expr = Regex(r'[+-]?(\d*\.\d+|\d+(\.\d+)?)([eE][+\-]?\d+)?(?=$|[ \t]*([\$\}\],#\n\r]|//))', - re.DOTALL).setParseAction(convert_number) + re.DOTALL).set_parse_action(convert_number) # multi line string using """ # Using fix described in http://pyparsing.wikispaces.com/share/view/3778969 - multiline_string = Regex('""".*?"*"""', re.DOTALL | re.UNICODE).setParseAction(parse_multi_string) + multiline_string = Regex('""".*?"*"""', re.DOTALL | re.UNICODE).set_parse_action(parse_multi_string) # single quoted line string - quoted_string = Regex(r'"(?:[^"\\\n]|\\.)*"[ \t]*', re.UNICODE).setParseAction(create_quoted_string) + quoted_string = Regex(r'"(?:[^"\\\n]|\\.)*"[ \t]*', re.UNICODE).set_parse_action(create_quoted_string) # unquoted string that takes the rest of the line until an optional comment # we support .properties multiline support which is like this: # line1 \ # line2 \ # so a backslash precedes the \n - unquoted_string = Regex(r'(?:[^^`+?!@*&"\[\{\s\]\}#,=\$\\]|\\.)+[ \t]*', re.UNICODE).setParseAction( + unquoted_string = Regex(r'(?:[^^`+?!@*&"\[\{\s\]\}#,=\$\\]|\\.)+[ \t]*', re.UNICODE).set_parse_action( unescape_string) - substitution_expr = Regex(r'[ \t]*\$\{[^\}]+\}[ \t]*').setParseAction(create_substitution) + substitution_expr = Regex(r'[ \t]*\$\{[^\}]+\}[ \t]*').set_parse_action(create_substitution) string_expr = multiline_string | quoted_string | unquoted_string value_expr = get_period_expr() | number_expr | true_expr | false_expr | null_expr | string_expr @@ -417,12 +381,12 @@ def set_default_white_spaces(): '(').suppress() - quoted_string - Literal(')').suppress()) ) include_expr = ( - Keyword("include", caseless=True).suppress() + ( + Keyword("include", case_insensitive=True).suppress() + ( include_content | ( Keyword("required") - Literal('(').suppress() - include_content - Literal(')').suppress() ) ) - ).setParseAction(include_config) + ).set_parse_action(include_config) root_dict_expr = Forward() dict_expr = Forward() @@ -451,7 +415,7 @@ def set_default_white_spaces(): config_expr = ZeroOrMore(comment_eol | eol) + ( list_expr | root_dict_expr | inside_root_dict_expr) + ZeroOrMore( comment_eol | eol_comma) - config = config_expr.parseString(content, parseAll=True)[0] + config = config_expr.parse_string(content, parse_all=True)[0] if resolve: allow_unresolved = resolve and unresolved_value is not DEFAULT_SUBSTITUTION \ @@ -542,7 +506,7 @@ def _find_substitutions(cls, item): """Convert HOCON input into a JSON output :return: JSON string representation - :type return: basestring + :type return: str """ if isinstance(item, ConfigValues): return item.get_substitutions() @@ -821,7 +785,7 @@ def postParse(self, instring, loc, token_list): if isinstance(value, list) and operator == "+=": value = ConfigValues([ConfigSubstitution(key, True, '', False, loc), value], False, loc) config_tree.put(key, value, False) - elif isinstance(value, unicode) and operator == "+=": + elif isinstance(value, str) and operator == "+=": value = ConfigValues([ConfigSubstitution(key, True, '', True, loc), ' ' + value], True, loc) config_tree.put(key, value, False) elif isinstance(value, list): diff --git a/pyhocon/config_tree.py b/pyhocon/config_tree.py index 52ac38c..5ca1657 100644 --- a/pyhocon/config_tree.py +++ b/pyhocon/config_tree.py @@ -4,12 +4,6 @@ import copy from pyhocon.exceptions import ConfigException, ConfigWrongTypeException, ConfigMissingException -try: - basestring -except NameError: # pragma: no cover - basestring = str - unicode = str - class UndefinedKey(object): pass @@ -219,7 +213,7 @@ def put(self, key, value, append=False): """Put a value in the tree (dot separated) :param key: key to use (dot separated). E.g., a.b.c - :type key: basestring + :type key: str :param value: value to put """ self._put(ConfigTree.parse_key(key), value, append) @@ -228,7 +222,7 @@ def get(self, key, default=UndefinedKey): """Get a value from the tree :param key: key to use (dot separated). E.g., a.b.c - :type key: basestring + :type key: str :param default: default value if key not found :type default: object :return: value in the tree located at key @@ -239,17 +233,17 @@ def get_string(self, key, default=UndefinedKey): """Return string representation of value found at key :param key: key to use (dot separated). E.g., a.b.c - :type key: basestring + :type key: str :param default: default value if key not found - :type default: basestring + :type default: str :return: string value - :type return: basestring + :type return: str """ value = self.get(key, default) if value is None: return None - string_value = unicode(value) + string_value = str(value) if isinstance(value, bool): string_value = string_value.lower() return string_value @@ -262,7 +256,7 @@ def pop(self, key, default=UndefinedKey): and pops the last value out of the dict. :param key: key to use (dot separated). E.g., a.b.c - :type key: basestring + :type key: str :param default: default value if key not found :type default: object :param default: default value if key not found @@ -286,7 +280,7 @@ def get_int(self, key, default=UndefinedKey): """Return int representation of value found at key :param key: key to use (dot separated). E.g., a.b.c - :type key: basestring + :type key: str :param default: default value if key not found :type default: int :return: int value @@ -303,7 +297,7 @@ def get_float(self, key, default=UndefinedKey): """Return float representation of value found at key :param key: key to use (dot separated). E.g., a.b.c - :type key: basestring + :type key: str :param default: default value if key not found :type default: float :return: float value @@ -320,7 +314,7 @@ def get_bool(self, key, default=UndefinedKey): """Return boolean representation of value found at key :param key: key to use (dot separated). E.g., a.b.c - :type key: basestring + :type key: str :param default: default value if key not found :type default: bool :return: boolean value @@ -347,7 +341,7 @@ def get_list(self, key, default=UndefinedKey): """Return list representation of value found at key :param key: key to use (dot separated). E.g., a.b.c - :type key: basestring + :type key: str :param default: default value if key not found :type default: list :return: list value @@ -374,7 +368,7 @@ def get_config(self, key, default=UndefinedKey): """Return tree config representation of value found at key :param key: key to use (dot separated). E.g., a.b.c - :type key: basestring + :type key: str :param default: default value if key not found :type default: config :return: config value @@ -522,7 +516,7 @@ def format_str(v, last=False): if isinstance(v, ConfigQuotedString): return v.value + ('' if last else v.ws) else: - return '' if v is None else unicode(v) + return '' if v is None else str(v) if self.has_substitution(): return self @@ -618,7 +612,7 @@ def __repr__(self): # pragma: no cover return '[ConfigSubstitution: ' + self.variable + ']' -class ConfigUnquotedString(unicode): +class ConfigUnquotedString(str): def __new__(cls, value): return super(ConfigUnquotedString, cls).__new__(cls, value) diff --git a/pyhocon/converter.py b/pyhocon/converter.py index 6e1eed0..ee40029 100644 --- a/pyhocon/converter.py +++ b/pyhocon/converter.py @@ -10,12 +10,6 @@ from pyhocon.config_tree import NoneValue from pyhocon.period_serializer import timedelta_to_str, is_timedelta_like, timedelta_to_hocon -try: - basestring -except NameError: - basestring = str - unicode = str - try: from dateutil.relativedelta import relativedelta except Exception: @@ -28,7 +22,7 @@ def to_json(cls, config, compact=False, indent=2, level=0): """Convert HOCON input into a JSON output :return: JSON string representation - :type return: basestring + :type return: str """ lines = "" if isinstance(config, ConfigTree): @@ -62,7 +56,7 @@ def to_json(cls, config, compact=False, indent=2, level=0): lines += '\n{indent}]'.format(indent=''.rjust(level * indent, ' ')) elif is_timedelta_like(config): lines += timedelta_to_str(config) - elif isinstance(config, basestring): + elif isinstance(config, str): lines = json.dumps(config, ensure_ascii=False) elif config is None or isinstance(config, NoneValue): lines = 'null' @@ -79,7 +73,7 @@ def to_hocon(cls, config, compact=False, indent=2, level=0): """Convert HOCON input into a HOCON output :return: JSON string representation - :type return: basestring + :type return: str """ lines = "" if isinstance(config, ConfigTree): @@ -120,7 +114,7 @@ def to_hocon(cls, config, compact=False, indent=2, level=0): value=cls.to_hocon(item, compact, indent, level + 1))) lines += '\n'.join(bet_lines) lines += '\n{indent}]'.format(indent=''.rjust((level - 1) * indent, ' ')) - elif isinstance(config, basestring): + elif isinstance(config, str): if '\n' in config and len(config) > 1: lines = '"""{value}"""'.format(value=config) # multilines else: @@ -154,7 +148,7 @@ def to_yaml(cls, config, compact=False, indent=2, level=0): """Convert HOCON input into a YAML output :return: YAML string representation - :type return: basestring + :type return: str """ lines = "" if isinstance(config, ConfigTree): @@ -182,7 +176,7 @@ def to_yaml(cls, config, compact=False, indent=2, level=0): lines += '\n'.join(bet_lines) elif is_timedelta_like(config): lines += timedelta_to_str(config) - elif isinstance(config, basestring): + elif isinstance(config, str): # if it contains a \n then it's multiline lines = config.split('\n') if len(lines) == 1: @@ -204,7 +198,7 @@ def to_properties(cls, config, compact=False, indent=2, key_stack=None): """Convert HOCON input into a .properties output :return: .properties string representation - :type return: basestring + :type return: str :return: """ key_stack = key_stack or [] @@ -224,7 +218,7 @@ def escape_value(value): lines.append(cls.to_properties(item, compact, indent, stripped_key_stack + [str(index)])) elif is_timedelta_like(config): lines.append('.'.join(stripped_key_stack) + ' = ' + timedelta_to_str(config)) - elif isinstance(config, basestring): + elif isinstance(config, str): lines.append('.'.join(stripped_key_stack) + ' = ' + escape_value(config)) elif config is True: lines.append('.'.join(stripped_key_stack) + ' = true') diff --git a/pyhocon/period_parser.py b/pyhocon/period_parser.py index efa7a48..fc25f1a 100644 --- a/pyhocon/period_parser.py +++ b/pyhocon/period_parser.py @@ -64,8 +64,8 @@ def get_period_expr(): return Combine( Word(nums)('value') + ZeroOrMore(Literal(" ")).suppress() + Or(period_types)('unit') + WordEnd( alphanums).suppress() - ).setParseAction(convert_period) + ).set_parse_action(convert_period) def parse_period(content): - return get_period_expr().parseString(content, parseAll=True)[0] + return get_period_expr().parse_string(content, parse_all=True)[0] diff --git a/setup.py b/setup.py index 7127c77..3218eb2 100755 --- a/setup.py +++ b/setup.py @@ -39,22 +39,19 @@ def run_tests(self): 'License :: OSI Approved :: Apache Software License', 'Topic :: Software Development :: Libraries :: Python Modules', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ], packages=[ 'pyhocon', ], install_requires=[ - 'pyparsing~=2.0;python_version<"3.0"', - 'pyparsing>=2,<4;python_version>="3.0"', + 'pyparsing>=3.0.0', ], extras_require={ 'Duration': ['python-dateutil>=2.8.0'] diff --git a/tox.ini b/tox.ini index 531e4c1..d3cffe1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8, py{27,38,39,310,311,312} +envlist = flake8, py{37,38,39,310,311,312,313} [testenv] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH