diff --git a/pyhocon/config_parser.py b/pyhocon/config_parser.py index 904a5ed0..9241de87 100644 --- a/pyhocon/config_parser.py +++ b/pyhocon/config_parser.py @@ -1,6 +1,7 @@ import codecs import contextlib import copy +import imp import itertools import logging import os @@ -29,7 +30,6 @@ def fixed_get_attr(self, item): pyparsing.ParseResults.__getattr__ = fixed_get_attr -import asset from pyhocon.config_tree import (ConfigInclude, ConfigList, ConfigQuotedString, ConfigSubstitution, ConfigTree, ConfigUnquotedString, ConfigValues, NoneValue) @@ -349,7 +349,7 @@ def include_config(instring, loc, token): if final_tokens[0] == 'url': url = value elif final_tokens[0] == 'package': - file = asset.load(value).filename + file = cls.resolve_package_path(value) else: file = value @@ -712,6 +712,25 @@ def resolve_substitutions(cls, config, accept_unresolved=False): cls._final_fixup(config) return has_unresolved + @classmethod + def resolve_package_path(cls, package_path): + """ + Resolve the path to a file inside a Python package. Expected format: "PACKAGE:PATH" + + Example: "my_package:foo/bar.conf" will resolve file 'bar.conf' in folder 'foo' + inside package 'my_package', which could result in a path like + '/path/to/.venv/lib/python3.7/site-packages/my_package/foo/bar.conf' + + :param package_path: the package path, formatted as "PACKAGE:PATH" + :return: the absolute path to the specified file inside the specified package + """ + if ':' not in package_path: + raise ValueError("Expected format is 'PACKAGE:PATH'") + package_name, path_relative = package_path.split(':', 1) + package_dir = imp.find_module(package_name)[1] + path_abs = os.path.join(package_dir, path_relative) + return path_abs + class ListParser(TokenConverter): """Parse a list [elt1, etl2, ...] diff --git a/setup.py b/setup.py index 6f6c2a38..62348a42 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ def run_tests(self): packages=[ 'pyhocon', ], - install_requires=['pyparsing>=2.0.3', 'asset'], + install_requires=['pyparsing>=2.0.3'], extras_require={ 'Duration': ['python-dateutil>=2.8.0'] }, diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index af833c33..a33e8029 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -2,12 +2,12 @@ import json import os +import shutil import tempfile from collections import OrderedDict from datetime import timedelta from pyparsing import ParseBaseException, ParseException, ParseSyntaxException -import asset import mock import pytest from pyhocon import (ConfigFactory, ConfigParser, ConfigSubstitutionException, ConfigTree) @@ -1267,26 +1267,42 @@ def test_include_missing_required_file(self): """ ) - def test_include_asset_file(self, monkeypatch): - with tempfile.NamedTemporaryFile('w') as fdin: - fdin.write('{a: 1, b: 2}') - fdin.flush() - - def load(*args, **kwargs): - class File(object): - def __init__(self, filename): - self.filename = filename - - return File(fdin.name) - - monkeypatch.setattr(asset, "load", load) - + def test_resolve_package_path(self): + path = ConfigParser.resolve_package_path("pyhocon:config_parser.py") + assert os.path.exists(path) + + def test_resolve_package_path_format(self): + with pytest.raises(ValueError): + ConfigParser.resolve_package_path("pyhocon/config_parser.py") + + def test_resolve_package_path_missing(self): + with pytest.raises(ImportError): + ConfigParser.resolve_package_path("non_existent_module:foo.py") + + def test_include_package_file(self, monkeypatch): + temp_dir = tempfile.mkdtemp() + try: + module_dir = os.path.join(temp_dir, 'my_module') + module_conf = os.path.join(module_dir, 'my.conf') + # create the module folder and necessary files (__init__ and config) + os.mkdir(module_dir) + open(os.path.join(module_dir, '__init__.py'), 'a').close() + with open(module_conf, 'w') as fdin: + fdin.write("{c: 3}") + # add the temp dir to sys.path so that 'my_module' can be discovered + monkeypatch.syspath_prepend(temp_dir) + # load the config and include the other config file from 'my_module' config = ConfigFactory.parse_string( """ - include package("dotted.name:asset/config_file") + a: 1 + b: 2 + include package("my_module:my.conf") """ ) - assert config['a'] == 1 + # check that the contents of both config files are available + assert dict(config.as_plain_ordered_dict()) == {'a': 1, 'b': 2, 'c': 3} + finally: + shutil.rmtree(temp_dir, ignore_errors=True) def test_include_dict(self): expected_res = {