expandvars-0.12.0/CODE_OF_CONDUCT.md0000644000000000000000000000643113615410400013470 0ustar00# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sayanarijit@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq expandvars-0.12.0/dependabot.yml0000644000000000000000000000076513615410400013525 0ustar00# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" expandvars-0.12.0/expandvars.py0000644000000000000000000003117213615410400013416 0ustar00# -*- coding: utf-8 -*- import os from io import TextIOWrapper __author__ = "Arijit Basu" __email__ = "sayanarijit@gmail.com" __homepage__ = "https://github.com/sayanarijit/expandvars" __description__ = "Expand system variables Unix style" __version__ = "v0.12.0" __license__ = "MIT" __all__ = [ "BadSubstitution", "ExpandvarsException", "MissingClosingBrace", "MissingExcapedChar", "NegativeSubStringExpression", "OperandExpected", "ParameterNullOrNotSet", "UnboundVariable", "expand", "expandvars", ] ESCAPE_CHAR = "\\" # Set EXPANDVARS_RECOVER_NULL="foo" if you want variables with # `${VAR:?}` syntax to fallback to "foo" if it's not defined. # Also works works with nounset=True. # # This helps with certain use cases where you need to temporarily # disable strict parsing of critical env vars. e.g. in testing # environment. # # See tests/test_recover_null.py for examples. # # WARNING: Try to avoid `export EXPANDVARS_RECOVER_NULL` as it # will permanently disable strict parsing until you log out. RECOVER_NULL = os.environ.get("EXPANDVARS_RECOVER_NULL", None) class ExpandvarsException(Exception): """The base exception for all the handleable exceptions.""" pass class MissingClosingBrace(ExpandvarsException, SyntaxError): def __init__(self, param): super().__init__("{0}: missing '}}'".format(param)) class MissingExcapedChar(ExpandvarsException, SyntaxError): def __init__(self, param): super().__init__("{0}: missing escaped character".format(param)) class OperandExpected(ExpandvarsException, SyntaxError): def __init__(self, param, operand): super().__init__( "{0}: operand expected (error token is {1})".format(param, repr(operand)) ) class NegativeSubStringExpression(ExpandvarsException, IndexError): def __init__(self, param, expr): super().__init__("{0}: {1}: substring expression < 0".format(param, expr)) class BadSubstitution(ExpandvarsException, SyntaxError): def __init__(self, param): super().__init__("{0}: bad substitution".format(param)) class ParameterNullOrNotSet(ExpandvarsException, KeyError): def __init__(self, param, msg=None): if msg is None: msg = "parameter null or not set" super().__init__("{0}: {1}".format(param, msg)) class UnboundVariable(ExpandvarsException, KeyError): def __init__(self, param): super().__init__("{0}: unbound variable".format(param)) def _valid_char(char): return char.isalnum() or char == "_" def _isint(val): try: int(val) return True except ValueError: return False def getenv(var, nounset, indirect, environ, default=None): """Get value from environment variable. When nounset is True, it behaves like bash's "set -o nounset" or "set -u" and raises UnboundVariable exception. When indirect is True, it will use the value of the resolved variable as the name of the final variable. """ val = environ.get(var) if val is not None and indirect: val = environ.get(val) if val: return val if default is not None: return default if nounset: if RECOVER_NULL is not None: return RECOVER_NULL raise UnboundVariable(var) return "" def escape(vars_, nounset, environ, var_symbol): """Escape the first character.""" if len(vars_) == 0: raise MissingExcapedChar(vars_) if len(vars_) == 1: return vars_[0] if vars_[0] == var_symbol: return vars_[0] + expand(vars_[1:], environ=environ, var_symbol=var_symbol) if vars_[0] == ESCAPE_CHAR: if vars_[1] == var_symbol: return ESCAPE_CHAR + expand( vars_[1:], nounset=nounset, environ=environ, var_symbol=var_symbol ) if vars_[1] == ESCAPE_CHAR: return ESCAPE_CHAR + escape( vars_[2:], nounset=nounset, environ=environ, var_symbol=var_symbol ) return ( ESCAPE_CHAR + vars_[0] + expand(vars_[1:], nounset=nounset, environ=environ, var_symbol=var_symbol) ) def expand_var(vars_, nounset, environ, var_symbol): """Expand a single variable.""" if len(vars_) == 0: return var_symbol if vars_[0] == ESCAPE_CHAR: return var_symbol + escape( vars_[1:], nounset=nounset, environ=environ, var_symbol=var_symbol ) if vars_[0] == var_symbol: return str(os.getpid()) + expand( vars_[1:], nounset=nounset, environ=environ, var_symbol=var_symbol ) if vars_[0] == "{": return expand_modifier_var( vars_[1:], nounset=nounset, environ=environ, var_symbol=var_symbol ) buff = [] for c in vars_: if _valid_char(c): buff.append(c) else: n = len(buff) return getenv( "".join(buff), nounset=nounset, indirect=False, environ=environ ) + expand( vars_[n:], nounset=nounset, environ=environ, var_symbol=var_symbol ) return getenv("".join(buff), nounset=nounset, indirect=False, environ=environ) def expand_modifier_var(vars_, nounset, environ, var_symbol): """Expand variables with modifier.""" if len(vars_) <= 1: raise BadSubstitution(vars_) if vars_[0] == "!": indirect = True vars_ = vars_[1:] else: indirect = False buff = [] for c in vars_: if _valid_char(c): buff.append(c) elif c == "}": n = len(buff) + 1 return getenv( "".join(buff), nounset=nounset, indirect=indirect, environ=environ ) + expand( vars_[n:], nounset=nounset, environ=environ, var_symbol=var_symbol ) else: n = len(buff) if c == ":": n += 1 return expand_advanced( "".join(buff), vars_[n:], nounset=nounset, indirect=indirect, environ=environ, var_symbol=var_symbol, ) raise MissingClosingBrace("".join(buff)) def expand_advanced(var, vars_, nounset, indirect, environ, var_symbol): """Expand substitution.""" if len(vars_) == 0: raise MissingClosingBrace(var) modifier = [] depth = 1 for c in vars_: if c == "{": depth += 1 modifier.append(c) elif c == "}": depth -= 1 if depth == 0: break else: modifier.append(c) else: modifier.append(c) if depth != 0: raise MissingClosingBrace(var) vars_ = vars_[len(modifier) + 1 :] modifier = expand( "".join(modifier), nounset=nounset, environ=environ, var_symbol=var_symbol ) if not modifier: raise BadSubstitution(var) if modifier[0] == "-": return expand_default( var, modifier=modifier[1:], set_=False, nounset=nounset, indirect=indirect, environ=environ, ) + expand(vars_, nounset=nounset, environ=environ, var_symbol=var_symbol) if modifier[0] == "=": return expand_default( var, modifier=modifier[1:], set_=True, nounset=nounset, indirect=indirect, environ=environ, ) + expand(vars_, nounset=nounset, environ=environ, var_symbol=var_symbol) if modifier[0] == "+": return expand_substitute( var, modifier=modifier[1:], environ=environ, ) + expand(vars_, nounset=nounset, environ=environ, var_symbol=var_symbol) if modifier[0] == "?": return expand_strict( var, modifier=modifier[1:], environ=environ, ) + expand(vars_, nounset=nounset, environ=environ, var_symbol=var_symbol) return expand_offset( var, modifier=modifier, nounset=nounset, environ=environ, ) + expand(vars_, nounset=nounset, environ=environ, var_symbol=var_symbol) def expand_strict(var, modifier, environ): """Expand variable that must be defined.""" val = environ.get(var, "") if val: return val if RECOVER_NULL is not None: return RECOVER_NULL raise ParameterNullOrNotSet(var, modifier if modifier else None) def expand_offset(var, modifier, nounset, environ): """Expand variable with offset.""" buff = [] for c in modifier: if c == ":": n = len(buff) + 1 offset_str = "".join(buff) if not offset_str or not _isint(offset_str): offset = 0 else: offset = int(offset_str) return expand_length( var, modifier=modifier[n:], offset=offset, nounset=nounset, environ=environ, ) buff.append(c) n = len(buff) + 1 offset_str = "".join(buff).strip() if not offset_str or not _isint(offset_str): offset = 0 else: offset = int(offset_str) return getenv(var, nounset=nounset, indirect=False, environ=environ)[offset:] def expand_length(var, modifier, offset, nounset, environ): """Expand variable with offset and length.""" length_str = modifier.strip() if not length_str: length = None elif not _isint(length_str): if not all(_valid_char(c) for c in length_str): raise OperandExpected(var, length_str) else: length = None else: length = int(length_str) if length < 0: raise NegativeSubStringExpression(var, length_str) if length is None: width = 0 else: width = offset + length return getenv(var, nounset=nounset, indirect=False, environ=environ)[offset:width] def expand_substitute(var, modifier, environ): """Expand or return substitute.""" if environ.get(var): return modifier return "" def expand_default(var, modifier, set_, nounset, indirect, environ): """Expand var or return default.""" if set_ and not environ.get(var): environ.update({var: modifier}) return getenv( var, nounset=nounset, indirect=indirect, default=modifier, environ=environ, ) def expand(vars_, nounset=False, environ=os.environ, var_symbol="$"): """Expand variables Unix style. Params: vars_ (str): Variables to expand. nounset (bool): If True, enables strict parsing (similar to set -u / set -o nounset in bash). environ (Mapping): Elements to consider during variable expansion. Defaults to os.environ var_symbol (str): Character used to identify a variable. Defaults to $ Returns: str: Expanded values. Example usage: :: from expandvars import expand print(expand("%PATH:$HOME/bin:%{SOME_UNDEFINED_PATH:-/default/path}", environ={"PATH": "/example"}, var_symbol="%")) # /example:$HOME/bin:/default/path # Or with open(somefile) as f: print(expand(f)) """ if isinstance(vars_, TextIOWrapper): # This is a file. Read it. vars_ = vars_.read().strip() if len(vars_) == 0: return "" buff = [] try: for c in vars_: if c == var_symbol: n = len(buff) + 1 return "".join(buff) + expand_var( vars_[n:], nounset=nounset, environ=environ, var_symbol=var_symbol ) if c == ESCAPE_CHAR: n = len(buff) + 1 return "".join(buff) + escape( vars_[n:], nounset=nounset, environ=environ, var_symbol=var_symbol ) buff.append(c) return "".join(buff) except MissingExcapedChar: raise MissingExcapedChar(vars_) except MissingClosingBrace: raise MissingClosingBrace(vars_) except BadSubstitution: raise BadSubstitution(vars_) def expandvars(vars_, nounset=False): """Expand system variables Unix style. Params: vars_ (str): System variables to expand. nounset (bool): If True, enables strict parsing (similar to set -u / set -o nounset in bash). Returns: str: Expanded values. Example usage: :: from expandvars import expandvars print(expandvars("$PATH:$HOME/bin:${SOME_UNDEFINED_PATH:-/default/path}")) # /bin:/sbin:/usr/bin:/usr/sbin:/home/you/bin:/default/path # Or with open(somefile) as f: print(expandvars(f)) """ return expand(vars_, nounset=nounset) expandvars-0.12.0/tox.ini0000644000000000000000000000032113615410400012174 0ustar00[tox] envlist = py36,py37,py38,py39,py310,py311,py312 [testenv] extras = tests whitelist_externals = black pytest commands = black --diff . pytest --cov --cov-report=html --cov-fail-under=100 expandvars-0.12.0/.github/workflows/tests.yml0000644000000000000000000000213013615410400016143 0ustar00name: Run Tests on: [push, pull_request] jobs: pytest: name: pytest runs-on: ubuntu-20.04 strategy: # You can use PyPy versions in python-version. # For example, pypy2 and pypy3 matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Run Pytest run: | pip install -e ".[tests]" pytest --cov --cov-fail-under=100 - name: Coverage uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true # optional (default = false) - name: Archive code coverage results uses: actions/upload-artifact@v3 with: name: code-coverage-report path: htmlcov expandvars-0.12.0/tests/__init__.py0000644000000000000000000000000013615410400014126 0ustar00expandvars-0.12.0/tests/test_expandvars.py0000644000000000000000000003101413615410400015612 0ustar00# -*- coding: utf-8 -*- import importlib from os import environ as env from os import getpid from unittest.mock import patch import pytest import expandvars @patch.dict(env, {}, clear=True) def test_expandvars_constant(): importlib.reload(expandvars) assert expandvars.expandvars("FOO") == "FOO" assert expandvars.expandvars("$") == "$" assert expandvars.expandvars("BAR$") == "BAR$" @patch.dict(env, {}, clear=True) def test_expandvars_empty(): importlib.reload(expandvars) assert expandvars.expandvars("") == "" assert expandvars.expandvars("$FOO") == "" @patch.dict(env, {"FOO": "bar"}, clear=True) def test_expandvars_simple(): importlib.reload(expandvars) assert expandvars.expandvars("$FOO") == "bar" assert expandvars.expandvars("${FOO}") == "bar" @patch.dict(env, {"FOO": "bar"}, clear=True) def test_expandvars_from_file(): importlib.reload(expandvars) with open("tests/data/foo.txt") as f: assert expandvars.expandvars(f) == "bar:bar" @patch.dict(env, {"FOO": "bar", "BIZ": "buz"}, clear=True) def test_expandvars_combo(): importlib.reload(expandvars) assert expandvars.expandvars("${FOO}:$BIZ") == "bar:buz" assert expandvars.expandvars("$FOO$BIZ") == "barbuz" assert expandvars.expandvars("${FOO}$BIZ") == "barbuz" assert expandvars.expandvars("$FOO${BIZ}") == "barbuz" assert expandvars.expandvars("$FOO-$BIZ") == "bar-buz" assert expandvars.expandvars("boo$BIZ") == "boobuz" assert expandvars.expandvars("boo${BIZ}") == "boobuz" @patch.dict(env, {}, clear=True) def test_expandvars_pid(): importlib.reload(expandvars) assert expandvars.expandvars("$$") == str(getpid()) assert expandvars.expandvars("PID( $$ )") == "PID( {0} )".format(getpid()) @patch.dict(env, {"ALTERNATE": "Alternate", "EMPTY": ""}, clear=True) def test_expandvars_get_default(): importlib.reload(expandvars) assert expandvars.expandvars("${FOO-default}") == "default" assert expandvars.expandvars("${FOO:-default}") == "default" assert expandvars.expandvars("${EMPTY:-default}") == "default" assert expandvars.expandvars("${FOO:-}") == "" assert expandvars.expandvars("${FOO:-foo}:${FOO-bar}") == "foo:bar" assert expandvars.expandvars("${FOO:-$ALTERNATE}") == "Alternate" assert expandvars.expandvars("${UNSET:-\\$foo}-\\$foo") == "$foo-$foo" @patch.dict(env, {"EMPTY": ""}, clear=True) def test_expandvars_update_default(): importlib.reload(expandvars) assert expandvars.expandvars("${FOO:=}") == "" assert expandvars.expandvars("${FOO=}") == "" assert expandvars.expandvars("${EMPTY:=}") == "" del env["FOO"] del env["EMPTY"] assert expandvars.expandvars("${FOO:=default}") == "default" assert expandvars.expandvars("${FOO=default}") == "default" assert expandvars.expandvars("${EMPTY:=default}") == "default" assert env.get("FOO") == "default" assert expandvars.expandvars("${FOO:=ignoreme}") == "default" assert expandvars.expandvars("${EMPTY:=ignoreme}") == "default" assert expandvars.expandvars("${FOO=ignoreme}:bar") == "default:bar" @patch.dict(env, {"FOO": "bar", "BUZ": "bar", "EMPTY": ""}, clear=True) def test_expandvars_substitute(): importlib.reload(expandvars) assert expandvars.expandvars("${FOO:+foo}") == "foo" assert expandvars.expandvars("${FOO+foo}") == "foo" assert expandvars.expandvars("${BAR:+foo}") == "" assert expandvars.expandvars("${BAR+foo}") == "" assert expandvars.expandvars("${EMPTY:+foo}") == "" assert expandvars.expandvars("${BAR:+}") == "" assert expandvars.expandvars("${BAR+}") == "" assert expandvars.expandvars("${BUZ:+foo}") == "foo" assert expandvars.expandvars("${BUZ+foo}:bar") == "foo:bar" assert expandvars.expandvars("${FOO:+${FOO};}") == "bar;" assert expandvars.expandvars("${BAR:+${BAR};}") == "" assert expandvars.expandvars("${BAR:+${EMPTY};}") == "" assert expandvars.expandvars("${FOO:+\\$foo}-\\$foo") == "$foo-$foo" @patch.dict(env, {"FOO": "damnbigfoobar"}, clear=True) def test_offset(): importlib.reload(expandvars) assert expandvars.expandvars("${FOO:3}") == "nbigfoobar" assert expandvars.expandvars("${FOO: 4 }") == "bigfoobar" assert expandvars.expandvars("${FOO:30}") == "" assert expandvars.expandvars("${FOO:0}") == "damnbigfoobar" assert expandvars.expandvars("${FOO: }") == "damnbigfoobar" assert expandvars.expandvars("${FOO: : }") == "" assert expandvars.expandvars("${FOO:-3}:bar") == "damnbigfoobar:bar" assert expandvars.expandvars("${FOO::}") == "" assert expandvars.expandvars("${FOO::aaa}") == "" assert expandvars.expandvars("${FOO: :2}") == "da" assert expandvars.expandvars("${FOO:aaa:2}") == "da" @patch.dict(env, {"FOO": "damnbigfoobar"}, clear=True) def test_offset_length(): importlib.reload(expandvars) assert expandvars.expandvars("${FOO:4:3}") == "big" assert expandvars.expandvars("${FOO: 7:6 }") == "foobar" assert expandvars.expandvars("${FOO:7: 100 }") == "foobar" assert expandvars.expandvars("${FOO:0:100}") == "damnbigfoobar" assert expandvars.expandvars("${FOO:70:10}") == "" assert expandvars.expandvars("${FOO:1:0}") == "" assert expandvars.expandvars("${FOO:0:}") == "" assert expandvars.expandvars("${FOO::}") == "" assert expandvars.expandvars("${FOO::5}") == "damnb" assert expandvars.expandvars("${FOO:-3:1}:bar") == "damnbigfoobar:bar" @patch.dict(env, {"FOO": "X", "X": "foo"}, clear=True) def test_expandvars_indirection(): importlib.reload(expandvars) assert expandvars.expandvars("${!FOO}:${FOO}") == "foo:X" assert expandvars.expandvars("${!FOO-default}") == "foo" assert expandvars.expandvars("${!BAR-default}") == "default" assert expandvars.expandvars("${!X-default}") == "default" @patch.dict(env, {"FOO": "foo", "BAR": "bar"}, clear=True) def test_escape(): importlib.reload(expandvars) assert expandvars.expandvars("\\$FOO\\$BAR") == "$FOO$BAR" assert expandvars.expandvars("\\\\$FOO") == "\\foo" assert expandvars.expandvars("$FOO\\$BAR") == "foo$BAR" assert expandvars.expandvars("\\$FOO$BAR") == "$FOObar" assert expandvars.expandvars("$FOO" "\\" "\\" "\\" "$BAR") == ("foo" "\\" "$BAR") assert expandvars.expandvars("$FOO\\$") == "foo$" assert expandvars.expandvars("$\\FOO") == "$\\FOO" assert expandvars.expandvars("$\\$FOO") == "$$FOO" assert expandvars.expandvars("\\$FOO") == "$FOO" assert ( expandvars.expandvars("D:\\\\some\\windows\\path") == "D:\\\\some\\windows\\path" ) @patch.dict(env, {}, clear=True) def test_corner_cases(): importlib.reload(expandvars) assert expandvars.expandvars("${FOO:-{}}{}{}{}{{}}") == "{}{}{}{}{{}}" assert expandvars.expandvars("${FOO-{}}{}{}{}{{}}") == "{}{}{}{}{{}}" @patch.dict(env, {}, clear=True) def test_strict_parsing(): importlib.reload(expandvars) with pytest.raises( expandvars.ExpandvarsException, match="FOO: parameter null or not set" ) as e: expandvars.expandvars("${FOO:?}") assert isinstance(e.value, expandvars.ParameterNullOrNotSet) with pytest.raises( expandvars.ExpandvarsException, match="FOO: parameter null or not set" ) as e: expandvars.expandvars("${FOO?}") assert isinstance(e.value, expandvars.ParameterNullOrNotSet) with pytest.raises(expandvars.ExpandvarsException, match="FOO: custom error") as e: expandvars.expandvars("${FOO:?custom error}") assert isinstance(e.value, expandvars.ParameterNullOrNotSet) with pytest.raises(expandvars.ExpandvarsException, match="FOO: custom error") as e: expandvars.expandvars("${FOO?custom error}") assert isinstance(e.value, expandvars.ParameterNullOrNotSet) env.update({"FOO": "foo"}) assert expandvars.expandvars("${FOO:?custom err}") == "foo" assert expandvars.expandvars("${FOO?custom err}:bar") == "foo:bar" @patch.dict(env, {"FOO": "foo"}, clear=True) def test_missing_escapped_character(): importlib.reload(expandvars) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("$FOO\\") assert str(e.value) == "$FOO\\: missing escaped character" assert isinstance(e.value, expandvars.MissingExcapedChar) @patch.dict(env, {"FOO": "damnbigfoobar"}, clear=True) def test_invalid_length_err(): importlib.reload(expandvars) with pytest.raises( expandvars.ExpandvarsException, match="FOO: -3: substring expression < 0" ) as e: expandvars.expandvars("${FOO:1:-3}") assert isinstance(e.value, expandvars.NegativeSubStringExpression) @patch.dict(env, {"FOO": "damnbigfoobar"}, clear=True) def test_bad_substitution_err(): importlib.reload(expandvars) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${FOO:}") assert str(e.value) == "${FOO:}: bad substitution" assert isinstance(e.value, expandvars.BadSubstitution) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${}") assert str(e.value) == "${}: bad substitution" assert isinstance(e.value, expandvars.BadSubstitution) @patch.dict(env, {"FOO": "damnbigfoobar"}, clear=True) def test_brace_never_closed_err(): importlib.reload(expandvars) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${FOO:") assert str(e.value) == "${FOO:: missing '}'" assert isinstance(e.value, expandvars.MissingClosingBrace) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${FOO}${BAR") assert str(e.value) == "${FOO}${BAR: missing '}'" assert isinstance(e.value, expandvars.MissingClosingBrace) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${FOO?") assert str(e.value) == "${FOO?: missing '}'" assert isinstance(e.value, expandvars.ExpandvarsException) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${FOO:1") assert str(e.value) == "${FOO:1: missing '}'" assert isinstance(e.value, expandvars.MissingClosingBrace) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${FOO:1:2") assert str(e.value) == "${FOO:1:2: missing '}'" assert isinstance(e.value, expandvars.MissingClosingBrace) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${FOO+") assert str(e.value) == "${FOO+: missing '}'" assert isinstance(e.value, expandvars.MissingClosingBrace) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${FOO-") assert str(e.value) == "${FOO-: missing '}'" assert isinstance(e.value, expandvars.MissingClosingBrace) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${FOO-{{}") assert str(e.value) == "${FOO-{{}: missing '}'" assert isinstance(e.value, expandvars.MissingClosingBrace) @patch.dict(env, {"FOO": "damnbigfoobar"}, clear=True) def test_invalid_operand_err(): importlib.reload(expandvars) oprnds = "@#$%^&*()'\"" for o in oprnds: with pytest.raises(expandvars.ExpandvarsException) as e: print(o) expandvars.expandvars("${{FOO:0:{0}}}".format(o)) assert str(e.value) == ("FOO: operand expected (error token is {0})").format( repr(o) ) assert isinstance(e.value, expandvars.OperandExpected) with pytest.raises(expandvars.ExpandvarsException) as e: expandvars.expandvars("${{FOO:{0}:{0}}}".format(o)) assert str(e.value) == ("FOO: operand expected (error token is {0})").format( repr(o) ) assert isinstance(e.value, expandvars.OperandExpected) @pytest.mark.parametrize("var_symbol", ["%", "&", "£", "="]) def test_expand_var_symbol(var_symbol): importlib.reload(expandvars) assert ( expandvars.expand( var_symbol + "{FOO}", environ={"FOO": "test"}, var_symbol=var_symbol ) == "test" ) assert ( expandvars.expand(var_symbol + "FOO", environ={}, var_symbol=var_symbol) == "" ) assert ( expandvars.expand( var_symbol + "{FOO:-default_value}", environ={}, var_symbol=var_symbol ) == "default_value" ) with pytest.raises(expandvars.ParameterNullOrNotSet): expandvars.expand(var_symbol + "{FOO:?}", environ={}, var_symbol=var_symbol) assert ( expandvars.expand( var_symbol + "{FOO},$HOME", environ={"FOO": "test"}, var_symbol=var_symbol ) == "test,$HOME" ) expandvars-0.12.0/tests/test_option_nounset.py0000644000000000000000000000210713615410400016523 0ustar00# -*- coding: utf-8 -*- import importlib from os import environ as env from unittest.mock import patch import pytest import expandvars @patch.dict(env, {}, clear=True) def test_expandvars_option_nounset(): importlib.reload(expandvars) assert expandvars.expandvars("$FOO") == "" with pytest.raises( expandvars.ExpandvarsException, match="FOO: unbound variable" ) as e: expandvars.expandvars("$FOO", nounset=True) assert isinstance(e.value, expandvars.UnboundVariable) with pytest.raises( expandvars.ExpandvarsException, match="FOO: unbound variable" ) as e: expandvars.expandvars("${FOO}", nounset=True) assert isinstance(e.value, expandvars.UnboundVariable) @patch.dict(env, {}, clear=True) def test_expandvars_option_nounset_with_strict(): importlib.reload(expandvars) with pytest.raises( expandvars.ExpandvarsException, match="FOO: parameter null or not set" ) as e: assert expandvars.expandvars("${FOO:?}", nounset=True) assert isinstance(e.value, expandvars.ParameterNullOrNotSet) expandvars-0.12.0/tests/test_recover_null.py0000644000000000000000000000110313615410400016132 0ustar00# -*- coding: utf-8 -*- import importlib from os import environ as env from unittest.mock import patch import expandvars @patch.dict(env, {"EXPANDVARS_RECOVER_NULL": "foo", "BAR": "bar"}, clear=True) def test_strict_parsing_recover_null(): importlib.reload(expandvars) assert expandvars.expandvars("${FOO:?}:${BAR?}") == "foo:bar" assert expandvars.expandvars("${FOO:?custom err}:${BAR?custom err}") == "foo:bar" assert expandvars.expandvars("$FOO$BAR", nounset=True) == "foobar" assert expandvars.expandvars("${FOO}:${BAR}", nounset=True) == "foo:bar" expandvars-0.12.0/tests/data/foo.txt0000644000000000000000000000001413615410400014257 0ustar00$FOO:${FOO} expandvars-0.12.0/.gitignore0000644000000000000000000000227713615410400012665 0ustar00# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # VSCode .vscodeexpandvars-0.12.0/LICENSE0000644000000000000000000000205413615410400011673 0ustar00MIT License Copyright (c) 2019 Arijit Basu 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. expandvars-0.12.0/README.md0000644000000000000000000000726513615410400012156 0ustar00# expandvars Expand system variables Unix style [![PyPI version](https://img.shields.io/pypi/v/expandvars.svg)](https://pypi.org/project/expandvars) [![codecov](https://codecov.io/gh/sayanarijit/expandvars/branch/master/graph/badge.svg)](https://codecov.io/gh/sayanarijit/expandvars) ## Inspiration This module is inspired by [GNU bash's variable expansion features](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html). It can be used as an alternative to Python's [os.path.expandvars](https://docs.python.org/3/library/os.path.html#os.path.expandvars) function. A good use case is reading config files with the flexibility of reading values from environment variables using advanced features like returning a default value if some variable is not defined. For example: ```toml [default] my_secret_access_code = "${ACCESS_CODE:-default_access_code}" my_important_variable = "${IMPORTANT_VARIABLE:?}" my_updated_path = "$PATH:$HOME/.bin" my_process_id = "$$" my_nested_variable = "${!NESTED}" ``` > NOTE: Although this module copies most of the common behaviours of bash, > it doesn't follow bash strictly. For example, it doesn't work with arrays. ## Installation ### Pip ``` pip install expandvars ``` ### Conda ``` conda install -c conda-forge expandvars ``` ## Usage ```python from expandvars import expandvars print(expandvars("$PATH:${HOME:?}/bin:${SOME_UNDEFINED_PATH:-/default/path}")) # /bin:/sbin:/usr/bin:/usr/sbin:/home/you/bin:/default/path ``` ## Examples For now, [refer to the test cases](https://github.com/sayanarijit/expandvars/blob/master/tests) to see how it behaves. ## TIPs ### nounset=True If you want to enable strict parsing by default, (similar to `set -u` / `set -o nounset` in bash), pass `nounset=True`. ```python # All the variables must be defined. expandvars("$VAR1:${VAR2}:$VAR3", nounset=True) # Raises UnboundVariable error. ``` > NOTE: Another way is to use the `${VAR?}` or `${VAR:?}` syntax. See the examples in tests. ### EXPANDVARS_RECOVER_NULL="foo" If you want to temporarily disable strict parsing both for `nounset=True` and the `${VAR:?}` syntax, set environment variable `EXPANDVARS_RECOVER_NULL=somevalue`. This helps with certain use cases where you need to temporarily disable strict parsing of critical env vars, e.g. in testing environment, without modifying the code. e.g. ```bash EXPANDVARS_RECOVER_NULL=foo myapp --config production.ini && echo "All fine." ``` > WARNING: Try to avoid `export EXPANDVARS_RECOVER_NULL` because that will disable strict parsing permanently until you log out. ### Customization You can customize the variable symbol and data used for the expansion by using the more general `expand` function. ```python from expandvars import expand print(expand("%PATH:$HOME/bin:%{SOME_UNDEFINED_PATH:-/default/path}", environ={"PATH": "/example"}, var_symbol="%")) # /example:$HOME/bin:/default/path ``` ## Contributing To contribute, setup environment following way: Then ```bash # Clone repo git clone https://github.com/sayanarijit/expandvars && cd expandvars # Setup virtualenv python -m venv .venv source ./.venv/bin/activate # Install as editable including test dependencies pip install -e ".[tests]" ``` - Follow [general git guidelines](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project). - Keep it simple. Run `black .` to auto format the code. - Test your changes locally by running `pytest` (pass `--cov --cov-report html` for browsable coverage report). - If you are familiar with [tox](https://tox.readthedocs.io), you may want to use it for testing in different python versions. ## Alternatives - [environs](https://github.com/sloria/environs) - simplified environment variable parsing. expandvars-0.12.0/pyproject.toml0000644000000000000000000000242213615410400013601 0ustar00[project] dynamic = ["version"] name = 'expandvars' description = 'Expand system variables Unix style' keywords = [ 'expand', 'system', 'variables', ] classifiers = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Intended Audience :: Other Audience', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python :: 3', 'Topic :: Utilities', 'Topic :: Software Development', 'Operating System :: MacOS', 'Operating System :: Unix', 'Operating System :: POSIX', 'Operating System :: Microsoft', ] homepage = 'https://github.com/sayanarijit/expandvars' authors = [ {name = "Arijit Basu", email = "sayanarijit@gmail.com"} ] maintainers = [ {name = "Arijit Basu", email = "sayanarijit@gmail.com"} ] readme = 'README.md' license = {file = "LICENSE"} requires-python = ">=3" [project.urls] "Homepage" = "https://github.com/sayanarijit/expandvars" [project.optional-dependencies] tests = ['tox', 'pytest', 'pytest-cov', 'black'] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.version] path = "expandvars.py" [tool.pytest] addopts = '--cov --cov-report=html --cov-fail-under=100' expandvars-0.12.0/PKG-INFO0000644000000000000000000001362013615410400011764 0ustar00Metadata-Version: 2.1 Name: expandvars Version: 0.12.0 Summary: Expand system variables Unix style Project-URL: Homepage, https://github.com/sayanarijit/expandvars Author-email: Arijit Basu Maintainer-email: Arijit Basu License: MIT License Copyright (c) 2019 Arijit Basu 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. License-File: LICENSE Keywords: expand,system,variables Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: Other Audience Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development Classifier: Topic :: Utilities Requires-Python: >=3 Provides-Extra: tests Requires-Dist: black; extra == 'tests' Requires-Dist: pytest; extra == 'tests' Requires-Dist: pytest-cov; extra == 'tests' Requires-Dist: tox; extra == 'tests' Description-Content-Type: text/markdown # expandvars Expand system variables Unix style [![PyPI version](https://img.shields.io/pypi/v/expandvars.svg)](https://pypi.org/project/expandvars) [![codecov](https://codecov.io/gh/sayanarijit/expandvars/branch/master/graph/badge.svg)](https://codecov.io/gh/sayanarijit/expandvars) ## Inspiration This module is inspired by [GNU bash's variable expansion features](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html). It can be used as an alternative to Python's [os.path.expandvars](https://docs.python.org/3/library/os.path.html#os.path.expandvars) function. A good use case is reading config files with the flexibility of reading values from environment variables using advanced features like returning a default value if some variable is not defined. For example: ```toml [default] my_secret_access_code = "${ACCESS_CODE:-default_access_code}" my_important_variable = "${IMPORTANT_VARIABLE:?}" my_updated_path = "$PATH:$HOME/.bin" my_process_id = "$$" my_nested_variable = "${!NESTED}" ``` > NOTE: Although this module copies most of the common behaviours of bash, > it doesn't follow bash strictly. For example, it doesn't work with arrays. ## Installation ### Pip ``` pip install expandvars ``` ### Conda ``` conda install -c conda-forge expandvars ``` ## Usage ```python from expandvars import expandvars print(expandvars("$PATH:${HOME:?}/bin:${SOME_UNDEFINED_PATH:-/default/path}")) # /bin:/sbin:/usr/bin:/usr/sbin:/home/you/bin:/default/path ``` ## Examples For now, [refer to the test cases](https://github.com/sayanarijit/expandvars/blob/master/tests) to see how it behaves. ## TIPs ### nounset=True If you want to enable strict parsing by default, (similar to `set -u` / `set -o nounset` in bash), pass `nounset=True`. ```python # All the variables must be defined. expandvars("$VAR1:${VAR2}:$VAR3", nounset=True) # Raises UnboundVariable error. ``` > NOTE: Another way is to use the `${VAR?}` or `${VAR:?}` syntax. See the examples in tests. ### EXPANDVARS_RECOVER_NULL="foo" If you want to temporarily disable strict parsing both for `nounset=True` and the `${VAR:?}` syntax, set environment variable `EXPANDVARS_RECOVER_NULL=somevalue`. This helps with certain use cases where you need to temporarily disable strict parsing of critical env vars, e.g. in testing environment, without modifying the code. e.g. ```bash EXPANDVARS_RECOVER_NULL=foo myapp --config production.ini && echo "All fine." ``` > WARNING: Try to avoid `export EXPANDVARS_RECOVER_NULL` because that will disable strict parsing permanently until you log out. ### Customization You can customize the variable symbol and data used for the expansion by using the more general `expand` function. ```python from expandvars import expand print(expand("%PATH:$HOME/bin:%{SOME_UNDEFINED_PATH:-/default/path}", environ={"PATH": "/example"}, var_symbol="%")) # /example:$HOME/bin:/default/path ``` ## Contributing To contribute, setup environment following way: Then ```bash # Clone repo git clone https://github.com/sayanarijit/expandvars && cd expandvars # Setup virtualenv python -m venv .venv source ./.venv/bin/activate # Install as editable including test dependencies pip install -e ".[tests]" ``` - Follow [general git guidelines](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project). - Keep it simple. Run `black .` to auto format the code. - Test your changes locally by running `pytest` (pass `--cov --cov-report html` for browsable coverage report). - If you are familiar with [tox](https://tox.readthedocs.io), you may want to use it for testing in different python versions. ## Alternatives - [environs](https://github.com/sloria/environs) - simplified environment variable parsing.