Source code for pytest_helper.pytest_helper_main

# -*- coding: utf-8 -*-
"""
Description
-----------

This module contains helper functions for using the pytest unit testing
framework.

..  Copyright (c) 2015 by Allen Barker.
    License: MIT, see LICENSE for more details.

.. default-role:: code

"""

# Replaced the depricated inspect.getargvalues fun, but the code is still commented out.
# Delete it after more testing.  Also ignored kwargs in locals_to_globals, too, and added
# an option to do those copies.
# https://www2.cs.duke.edu/acm-docs/python/python-3.5.2-docs-html/library/inspect.html#inspect.getargvalues

# Possible future enhancements.
#
# 1) Go up the directory tree and, in addition to
#    looking for config files, note the project root directory and the package
#    root directory (one above the project root).  Then allow relative addresses
#    such as:
#       pytest_helper.sys_path("{proj_root}/test")
#       pytest_helper.sys_path("{pkg_root}/pkg_subdir")
#    As a simpler related enhancement, just allow globs in paths by passing through
#    the python glob function.
#
# 2) Integrate with pudb debugger, maybe via a kwarg to script_run.  Without pudb plugin
#    it works like this, but could be a single kwarg:
#       pytest_args="--pdbcls pudb.debugger:Debugger --pdb -s")
#
# 3) Consider using pytest-helper-namespace to put the helper function into the
#    pytest namespace.  Might be more convenient, or might be overkill.  May not
#    buy you much, just pytest.helpers.script_run after still importing the
#    pytest_helper package (or putting it into some config file).  Or maybe
#    consider making into a plugin (probably overkill).
#
# 4) See the notes in q-dir about usage, etc.  Add a snippet file to the docs.

# Consider the different use-cases for choosing the defaults.
# Note current defaults make modify_syspath=None, which
# which does removal it only if inside a package.
#   - test-calling scripts, meant to be in package module
#   - test-calling scripts, meant to be in module outside the package, but
#        maybe being developed there first
#   - self-test scripts inside and outside a package
# But note that pytest itself will load a package if necessary.

from __future__ import print_function, division, absolute_import
import inspect
import sys
import os
import set_package_attribute

try:
    import pytest
except ImportError:
    import py.test as pytest # Old pytest versions, before 3.0.
from pytest_helper.config_file_handler import (get_config_value, get_config)

from pytest_helper.global_settings import (PytestHelperException,
                                           LocalsToGlobalsError,
                                           ALLOW_USER_CONFIG_FILES,
                                           NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT)

[docs]def script_run(testfile_paths=None, self_test=False, pytest_args=None, pyargs=False, modify_syspath=None, calling_mod_name=None, calling_mod_path=None, single_call=True, exit=True, always_run=False, skip=False, pskip=False, level=2): """Run pytest on the specified test files when the calling module is run as a script. Using this function requires at least pytest 2.0. If the module from which this script is called is not `__main__` then this script immediately returns and does nothing (unless `always_run` is set true). The argument `testfile_paths` should be either the pathname of a file or directory to run pytest on, or else a list of such file and directory paths. Any relative paths will be interpreted relative to the directory of the module which calls this function. The calculation of relative paths can fail in cases where Python's CWD is changed between the time when the calling module is loaded and a pytest-helper function is called. (Most programs do not change the CWD like that, or if they do they return it to its previous value.) In those cases the `pytest_helper.init()` function can be called just after importing `pytest_helper` (or absolute pathnames can be used). The recommended use of `script_run` is to place it inside a guard conditional which runs only for scripts, and to call it before doing any other non-system imports. The early call avoids possible problems with relative imports when running it from inside modules that are part of packages. The use of the guard conditional is optional, but is more explicit and slightly more efficient. If `self_test` is `True` then pytest will be run on the file of the calling script itself, i.e., tests are assumed to be in the same file as the code to test. The `pytest_args` allows command-line arguments to be passed to pytest when it is run. It can be set to a string of all the options or else a list of strings (with one item in the list for each flag and for each flag argument). Options containing non-separator spaces, such as whitespace in quoted filenames, are currently not allowed in the string form. In that case the list form should be used. The pytest command-line argument `--pyargs` allows a mix of filenames and Python package names to be passed to pytest as test files. Note that pytest *always* imports modules as part of a package if there is an `__init__.py` file in the directory; the `--pyargs` just allows Python-style module names. When `pyargs` is set true pytest will be run with the `--pyargs` option set, and any items in `testfile_paths` which contain no path-separator character (slash) will be left unprocessed rather than being converted into absolute pathnames. The pytest option `--pyargs` will not work correctly unless this flag is set. The default is `pyargs=False`, i.e., by default all paths are converted to absolute pathnames. It usually will not matter, but in this mode you can specify a directory name relative to the current directory and not have it treated as a Python module name by using `./dirname` rather than simply `filename`. If `modify_syspath` is explicitly set `True` then the first item in the `sys.path` list is deleted, but only if it has not been deleted before (by this package or by the `set_package_attribute` package). If `modify_syspath` is set false then the system path is not modified. The default is `None`, which modifies the system path if the calling module is part of a package and otherwise does not. The reason for this option is that when a module is run as a script Python always adds the directory of the script as the first item in `sys.path`. This can sometimes cause hard-to-trace import errors when directories inside paths are inserted in `sys.path`. Deleting that added directory first prevents those errors. If `script_run` does not call exit at the end (because `exit==False`) then, before returning, any modified system path is restored to a saved copy of its full, original condition. The `calling_mod_name` argument is a fallback in case the calling function's module is not correctly located by introspection. It is usually not required (though it is slightly more efficient). Use it as: `module_name=__name__`. Similarly for `calling_mod_path`, but that should be passed the pathname of the calling module's file. By default the script-run program passes all the user-specified testfile paths to a single run of pytest. If `single_call` is set false then instead the paths are looped over, one by one, with a separate call to pytest on each one. If `exit` is set false `sys.exit(0)` will not be called after the tests finish. The default is to exit after the tests finish (otherwise when tests run from the top of a module are finished the rest of the file will still be executed). Setting `exit` false can be used to make several separate `script_run` calls in sequence. If `always_run` is true then tests will be run regardless of whether or not the function was called from a script. If `skip` is set to true from the default false then the function returns immediately without doing anything. This is just a keyword argument switch that can be used to temporarily turn off test-running without large changes to the code. The module runs normally without pytest being invoked. The `pskip` option is the same, except that it also sets the package attribute via `set_package_attribute`. This option is useful if the script is being run inside a package, since it allows relative imports to be used in the script. (In this case the `modify_syspath` argument is passed to the `init` function of `set_package_attribute`). The parameter `level` is the level up the calling stack to look for the calling module and should not usually need to be set.""" if skip: return if pskip: set_package_attribute.init(modify_syspath) return mod_info = get_calling_module_info(module_name=calling_mod_name, module_path=calling_mod_path, level=level) calling_mod_name, calling_mod, calling_mod_path, calling_mod_dir, in_pkg = mod_info if calling_mod_name != "__main__" and not always_run: return def convert_arg_string_to_list(arg_list_or_string): """Convert string pytest_args arguments to a list, keeping lists unchanged.""" if not arg_list_or_string: pytest_arglist = [] elif isinstance(arg_list_or_string, str): pytest_arglist = arg_list_or_string.split() else: pytest_arglist = arg_list_or_string return pytest_arglist # Override arguments with any values set in the config file. pytest_arglist = convert_arg_string_to_list(pytest_args) pytest_arglist = convert_arg_string_to_list( # These override passed-in args. get_config_value("script_run_pytest_args", pytest_arglist, calling_mod, calling_mod_dir)) pytest_arglist += convert_arg_string_to_list( # These are added to passed-in args. get_config_value("script_run_extra_pytest_args", [], calling_mod, calling_mod_dir)) if modify_syspath or (modify_syspath is None and in_pkg): set_package_attribute._delete_sys_path_0() if isinstance(testfile_paths, str): testfile_paths = [testfile_paths] elif testfile_paths is None: testfile_paths = [] if self_test: testfile_paths.append(calling_mod_path) testfile_paths = [os.path.expanduser(p) for p in testfile_paths] # If pyargs is set, don't expand any arguments which do not have a slash in # them. In that case it was not a relative pathname anyway (except to # current dir, which necessitates the use of "./filename" rather than # "filename"). Will be treated as a package if pyargs=True. if pyargs: testfile_paths = [expand_relative(p, calling_mod_dir) if os.path.sep in p else p for p in testfile_paths] else: testfile_paths = [expand_relative(p, calling_mod_dir) for p in testfile_paths] # Add "--pyargs" to arguments if not there and no flag not to. if pyargs and "--pyargs" not in pytest_arglist: pytest_arglist.append("--pyargs") # Generate calling string and call pytest on the file. if single_call: pytest.main(pytest_arglist + testfile_paths) else: for testfile in testfile_paths: # Call pytest main; this requires pytest 2.0 or greater. pytest.main(pytest_arglist + [testfile]) if exit: sys.exit(0)
#if syspath_modified: # Not exiting, so restore the system path if modified. # set set_package_attribute._restore_sys_path0() # NOTE: No longer restoring on non-exit. previous_sys_path_list = None # Save the sys.path before modifying it, to restore it.
[docs]def sys_path(dirs_to_add=None, add_parent=False, add_grandparent=False, add_gn_parent=False, add_self=False, insert_position=1, calling_mod_name=None, calling_mod_path=None, level=2): r"""Add the canonical absolute pathname of each directory in the list `dirs_to_add` to `sys.path` (but only if it isn't there already). A single string representing a path can also be passed to `dirs_to_add`. Relative pathnames are always interpreted relative to the directory of the calling module (i.e., the directory of the module that calls this function). The notes about relative paths for the `script_run` function also apply here. The keyword arguments `add_parent` and `add_grandparent` are shortcuts that can be used instead of putting the equivalent relative path on the list `dirs_to_add`. If the keyword argument `add_gn_parent` is set to a non-negative integer `n` then the (grand)\ :sup:`n`\ parent is added to the path, where (grand)\ :sup:`1`\ parent is the grandparent. If `add_self` is true then the directory of the calling module is added to the system `sys.path` list. The keyword argument `insert_position` determines where in `sys.path` the the pathnames are placed (using `insert`). The default is 1. If inserting at 0 watch for conflicts with the `modify_syspath` options to `script_run` and `init`. The parameters `calling_mod_name` and `calling_mod_dir` can be set as a fallback in case the introspection for finding the calling module's information fails for some reason. The parameter `level` is the level up the calling stack to look for the calling module and should not usually need to be set.""" # We really only need calling_mod_dir as a fallback *except* when config # files are used we also need to know the module path, since config info # is looked up and saved per-module. mod_info = get_calling_module_info(module_name=calling_mod_name, module_path=calling_mod_path, level=level) calling_mod_name, calling_mod, calling_mod_path, calling_mod_dir, in_pkg = mod_info if dirs_to_add is None: dirs_to_add = [] if isinstance(dirs_to_add, str): dirs_to_add = [dirs_to_add] if add_parent: dirs_to_add.append("..") if add_grandparent: dirs_to_add.append(os.path.join("..","..")) if add_gn_parent is not False: try: g_level = int(add_gn_parent) except TypeError: raise PytestHelperException("Non-integer argument to the add_gn_parent " "argument of the pytest_helper.sys_path function.") if g_level < 0 or add_gn_parent is True: raise PytestHelperException("Negative argument or literal True argument\n" "to the add_gn_parent parameter of the pytest_helper.sys_path" " function.\nIt must be a non-negative integer.") parent_string = ".." for i in range(g_level): parent_string = os.path.join(parent_string, "..") dirs_to_add.append(parent_string) if add_self: dirs_to_add.append(".") dirs_to_add = [os.path.expanduser(p) for p in dirs_to_add] global previous_sys_path_list previous_sys_path_list = sys.path[:] for path in reversed(dirs_to_add): # Reverse since all inserted at insert_position. if os.path.isabs(path): path = os.path.realpath(path) # Convert to canonical path. if path not in sys.path: sys.path.insert(insert_position, path) else: joined_path = expand_relative(path, calling_mod_dir) if joined_path not in sys.path: sys.path.insert(insert_position, joined_path) return
[docs]def restore_previous_sys_path(): """This function undoes the effect of the last call to `sys_path`, returning `sys.path` to its previous, saved value. This can be useful at times.""" global previous_sys_path_list if previous_sys_path_list is not None: sys.path = previous_sys_path_list previous_sys_path_list = None
[docs]def init(modify_syspath=None, conf=True, calling_mod_name=None, calling_mod_path=None, level=2): """A function to initialize the `pytest_helper` module just after importing it. This function is useful, for example, in rare cases where Python's current working directory (CWD) is changed between the time when the executing module is first loaded and when `script_run` or `sys_path` is called from that module. In those cases the module's pathname relative to the previous CWD will be incorrectly expanded relative to the new CWD. Calling this function causes the earlier-expanded pathname to be cached. This function should be called before any function call or import which changes the CWD and which doesn't change it back afterward. Importing `pytest_helper` just after the system imports and then immediately calling this function should work. The `modify_syspath` option affects whether or not the `sys.path[0]` element is removed. The default `None` is to remove it for scripts run from inside packages since that can mess up the imports of modules running as packages. If the parameter `conf` is set false then no configuration files will be searched for or used. Otherwise, the configuration file will be searched for by any function which has an option settable in a config file (including the `init` function itself). The configuration information for modules is cached, and so is only looked up once. """ # The get_calling_module_info function caches the module info, including the # calling_mod_path, as a side effect. mod_info = get_calling_module_info(module_name=calling_mod_name, module_path=calling_mod_path, level=level) calling_mod_name, calling_mod, calling_mod_path, calling_mod_dir, in_pkg = mod_info if calling_mod_name == "__main__": # Disable the configuration file if requested. if not conf or not ALLOW_USER_CONFIG_FILES: get_config(calling_mod, calling_mod_dir, disable=True) # Handle the modify_syspath options. if modify_syspath or (modify_syspath is None and in_pkg): set_package_attribute._delete_sys_path_0()
# # Functions for copying locals to globals. #
[docs]def locals_to_globals(fun_locals=None, fun_globals=None, clear=False, noclobber=True, ignore_params=True, level=2): """Copy all local variables in the calling test function's local scope to the global scope of the module from which that function was called. The test function's parameters are ignored (i.e., they are local variables but they are not made global). Setting `ignore_params` false copies them, too. This routine should generally be called near the end of a test function or fixture. It allows for variables to be shared with other test functions, as globals. Calls to `locals_to_globals` do not allow existing global variables to be overwritten unless they were either 1) set by a previous run of this function, or 2) `noclobber` is set false. Otherwise a `LocalsToGlobalsError` will be raised. This avoids accidentally overwriting important global attributes (especially when tests are in the same module being tested). This routine's effect is similar to the effect of explicitly declaring each of a function's local variables to be `global`, or doing `globals().update(locals())`, except that 1) it ignores local variables which are function parameters, 2) it adds more error checks, and 3) it can clear any previously-set values. Note that the globals set with `locals_to_globals` can be accessed and used in any other test function in the module, but they are still read-only (as usual with globals). An attribute must be explicitly declared `global` in order to modify the global value. (It is then no longer local to the function, so `locals_to_globals` will not affect it, but `clear` will still remember it and clear it if called.) If `clear` is true (the default is false) then any variable that was set on the last run of this function will be automatically cleared before any new ones are set. This is good to call in the first-run fixture or setup function, since it helps avoid "false positives" where a later test succeeds only because of a global left over from a previous test. Note globals on the saved list of globals are cleared even if their values were later modified. The argument `fun_locals` can be used as a fallback to pass the `locals()` dict from the function in case the introspection technique does not work for some reason. The `fun_globals` argument can similarly be passed globals() as a fallback. So you could call:: locals_to_globals(locals(), globals()) to bypass the introspection used to locate the two dicts. The `level` argument is the level up the calling stack to look for the calling function. In order to call an intermediate function which then calls this function, for example, `level` would need to be increased by one.""" #view_locals_up_stack(4) # useful for debugging if not fun_locals: fun_locals = get_calling_fun_locals_dict(level) if not fun_globals: fun_globals = get_calling_fun_globals_dict(level) if NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT not in fun_globals: fun_globals[NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT] = {} module_info_dict = fun_globals[NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT] if "list_of_globals_copied_to_locals" in module_info_dict: globals_copied_to_list = module_info_dict["list_of_globals_copied_to_locals"] else: globals_copied_to_list = [] module_info_dict["list_of_globals_copied_to_locals"] = globals_copied_to_list if clear: clear_locals_from_globals(level=level+1) # One extra level from this fun. # Get the function's parameters so we can ignore them as locals. #params, values = get_calling_fun_parameters(level) args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations = \ get_calling_fun_parameters(level) params = list(args) if args else [] params = params + varargs if varargs else params params = params + varkw if varkw else params params = params + kwonlyargs if kwonlyargs else params # Do the actual copies. for k, v in fun_locals.items(): if k in params and ignore_params: continue # The following line filters out some weird pytest vars starting with @. if not (k[0].isalpha() or k[0] == "_"): continue if k in fun_globals and noclobber and k not in globals_copied_to_list: raise LocalsToGlobalsError("Attempt to overwrite existing" " module-global variable '{0}'. The current value is" " {1}. Attempted to overwrite with a value of {2}." .format(k, str(fun_globals[k]), str(v))) fun_globals[k] = fun_locals[k] globals_copied_to_list.append(k) return
[docs]def clear_locals_from_globals(level=2): """Clear all the global variables that were added by locals_to_globals. This is called automatically by `locals_to_globals` unless that function is run with `clear` set false. This only affects the module from which the function is called.""" g = get_calling_fun_globals_dict(level) if NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT not in g: return globals_copied_to_list = g[NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT].get( "list_of_globals_copied_to_locals", []) for k in globals_copied_to_list: try: del g[k] except KeyError: pass # Ignore if not there. del globals_copied_to_list[:] # Empty out globals_copied_to_list in-place.
[docs]def unindent(unindent_level, string): """Strip indentation from a docstring. This function is useful in tests where you have assertions that something equals a multi-line string. It allows the strings to be represented as multi-line docstrings but indented in a way that matches the surrounding code. Calling this function on a string 1) splits it into lines (keeping empty lines, too), 2) discards the first and last line, and 3) removes `unindent_level` characters from the beginning of each line. Then 4) the modified lines are joined with newline and returned. Raises an exception on an attempt to strip non-whitespace or if there are fewer than two lines.""" # Use split instead of splitlines because splitlines drops an single trailing # newline if any are there. lines = string.split("\n") if len(lines) < 2: raise PytestHelperException("String argument to unindent must have at least" " two lines, since the first and last lines are discarded. The string" " argument was: '{0}'".format(string)) lines = lines[1:-1] # Discard first and last line. for line in lines: string_to_strip = line[0:unindent_level] if not string_to_strip.lstrip() == "": raise PytestHelperException("Attempt to unindent non-whitespace at" " the beginning of this line:\n'{0}'".format(line)) stripped = "\n".join(s[unindent_level:] for s in lines) return stripped
autoimport_DEFAULTS = [("pytest", pytest), # (<nameToImportAs>, <value>) ("raises", pytest.raises), ("fail", pytest.fail), ("fixture", pytest.fixture), ("skip", pytest.skip), ("xfail", pytest.xfail), ("approx", pytest.approx), ("locals_to_globals", locals_to_globals), ("clear_locals_from_globals", clear_locals_from_globals), ("unindent", unindent), ]
[docs]def autoimport(noclobber=True, skip=None, calling_mod_name=None, calling_mod_path=None, level=2): """This function imports some pytest-helper and pytest attributes into the calling module's global namespace. This avoids having to explicitly do common imports. Even if `autoimport` is called from inside a test function it will still place its imports in the module's global namespace. A `PytestHelperException` will be raised if any of those globals already exist, unless `noclobber` is set false. The `imports` option is a list of (name,value) pairs to import automatically. Since using it each time would be as much trouble as doing the imports, explicitly, it is mainly meant to be set in configuration files. In a config file the values are evaluated in the global namespace of `pytest_helper`. The `skip` option is a list of names to skip in importing, if just one or two are causing problems locally to a file. The default variables that are imported from the `pytest_helper` module are `locals_to_globals`, `clear_locals_from_globals`, and `unindent`. The module `pytest` is imported as `pytest`. The functions from pytest that are imported by default are `raises`, `fail`, `fixture`, and `skip`, `xfail`, and `approx`.""" mod_info = get_calling_module_info(module_name=calling_mod_name, module_path=calling_mod_path, level=level) calling_mod_name, calling_mod, calling_mod_path, calling_mod_dir, in_pkg = mod_info # Override arguments with any values set in the config file. noclobber = get_config_value("autoimport_noclobber", noclobber, calling_mod, calling_mod_dir) skip = get_config_value("autoimport_skip", skip, calling_mod, calling_mod_dir) def insert_in_dict(d, name, value, noclobber): """Insert (name, value) in dict d checking for noclobber.""" if noclobber and name in d: raise PytestHelperException("The pytest_helper function autoimport" "\nattempted an overwrite with noclobber set. The attribute" " is: " + name) try: d[name] = value except KeyError: raise g = get_calling_fun_globals_dict(level=level) for name, value in autoimport_DEFAULTS: if skip and name in skip: continue insert_in_dict(g, name, value, noclobber)
# # Utility functions. # def expand_relative(path, basepath): """Expand the path `path` relative to the path `basepath`. If `basepath` is not an absolute path it is first expanded relative to Python's current CWD to be one. The canonical version of the absolute path is returned.""" path = os.path.expanduser(path) if os.path.isabs(path): return os.path.realpath(path) # Return canonical path, already absolute. if not os.path.isabs(basepath): basepath = os.path.realpath(os.path.abspath(basepath)) joined_path = os.path.realpath(os.path.abspath(os.path.join(basepath, path))) return joined_path # The levels used in the utility routines below are levels in the calling stack # (examined using inspect). The level number includes the level of the utility # function itself. So level 0 is the attribute of the utility function itself, # level 1 is the attribute of the calling function, level 2 is the function # that called the calling function, etc. module_info_cache = {} # Save info on modules, keyed on module names. def get_calling_module_info(level=2, check_exists=True, module_name=None, module_path=None): """A higher-level routine to get information about the module of a function back some number of levels in the call stack (the calling function). Returns a tuple:: (calling_module_name, calling_module, calling_module_path, calling_module_dir, in_pkg) Any relative paths are converted to absolute paths. If `check_exists` is true then a check is made to make sure that the module actually exists at the path. Absolute paths are cached in a dict keyed on module names so we always get the pathname calculated on the first call to this program from a given module. This is important in cases where the CWD is changed between the initial loading time for a module and the time it (indirectly) calls this routine. Such problems are rare, but if they occur you can use these two lines near the top of the module (before any imports which might change CWD):: import pytest_helper pytest_helper.get_calling_module_info(level=1) The module name and/or path can be supplied via the keyword arguments if introspection still fails for some reason (or just to slightly improve efficiency).""" # Note that caching of looked-up calling module info is done using the # fully-qualified module name as the key (just like sys.modules uses). Not # everything can be cached, but the calling_module_dir is one thing that is # cached. # # Note that __main__ module will not be cached the same as when run from # pytest with its normal module name. That shouldn't be a problem though. if module_name: calling_module_name = module_name calling_module = sys.modules[calling_module_name] else: # Call low-level routine to do the introspection. calling_module_name, calling_module = get_calling_module(level) if module_path: calling_module_path = module_path elif calling_module_name in module_info_cache: return module_info_cache[calling_module_name] elif hasattr(calling_module, "__file__"): calling_module_path = os.path.realpath( os.path.abspath(calling_module.__file__)) else: # No __file__ attribute in __main__ (probably interactive running). # Workaround, see: https://bugs.python.org/issue12920 frame = inspect.stack()[level][0] calling_module_path = os.path.realpath(os.path.abspath(inspect.getfile(frame))) calling_module_dir = os.path.dirname(calling_module_path) # Do an error check to make sure that the detected module directory exists. if check_exists and not os.path.isdir(calling_module_dir): raise PytestHelperException("\n\nThe directory\n {0}\nof the detected" " calling module does not exist.\nDid the Python CWD change without" " being changed back?\nYou can try importing pytest_helper near the" " top\nof your file and then immediately calling\n" " pytest_helper.init()\nafterward. If" " necessary you can set `module_name` and\n`module_path`" " explicitly from keyword arguments.".format(calling_module_dir)) in_pkg = os.path.exists(os.path.join(calling_module_dir, "__init__.py")) module_info = (calling_module_name, calling_module, calling_module_path, calling_module_dir, in_pkg) module_info_cache[calling_module_name] = module_info return module_info def view_locals_up_stack(num_levels=4): """Just to get an idea of what things look like. Run from somewhere and see.""" print("Viewing local variable dict keys up the stack.\n") for i in reversed(range(num_levels)): calling_fun_frame = inspect.stack()[i][0] calling_fun_name = inspect.stack()[i][3] calling_fun_locals = calling_fun_frame.f_locals indent = " " * (num_levels - i) print("{0}{1} -- stack level {2}".format(indent, calling_fun_name, i)) print("{0}f_locals keys: {1}\n".format(indent, calling_fun_locals.keys())) def show_globals(level=2, filt=True): """Prints the module globals for the module the calling function was called in, filtering out files starting with '__'. Useful for testing.""" print("Globals:") g = get_calling_fun_globals_dict(level) for k, v in sorted(g.items()): if filt and k.startswith("__"): continue print(" {0} = {1}".format(k, v)) def get_calling_fun_parameters(level=2): """Note that in calling this function you have to increase the level by one since it is also on the stack when it does the lookup. level 0: This function. level 1: The frame of the function that called this function. level 2: The frame of the function that called the calling function.""" calling_fun_frame = inspect.stack()[level][0] #params, _, _, values = inspect.getargvalues(calling_fun_frame) #return (params, values) args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations = ( inspect.getfullargspec(calling_fun_frame)) return args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations def get_calling_fun_locals_dict(level=2): """Note that in calling this function you have to increase the level by one since it is also on the stack when it does the lookup. level 0: The locals dict of this function. level 1: The locals dict of the function that called this function. level 2: The locals dict of the function that called the calling function.""" calling_fun_frame = inspect.stack()[level][0] calling_fun_locals = calling_fun_frame.f_locals return calling_fun_locals def get_calling_fun_globals_dict(level=2): """Note that in calling this function you have to increase the level by one since it is also on the stack when it does the lookup. level 0: The globals dict of this function. level 1: The globals dict of the function that called this function. level 2: The globals dict of the function that called the calling function.""" calling_fun_frame = inspect.stack()[level][0] calling_fun_globals = calling_fun_frame.f_globals return calling_fun_globals def get_calling_module(level=2): """Run this inside a function. Get the module where the current function is being called from. Returns a tuple (mod_name, module) with the name of the module and the module itself. Note that the module name may be relative (to the CWD when the module was loaded) or absolute. For some info on the introspection methods used, see stackoverflow.com/questions/1095543/. level 0: Module for this function (i.e., this module). level 1: Module for the function calling this one (what you usually want). level 2: Module for the function that called the one calling this one. This is a low-level routine; higher-level functions should usually call `get_calling_module_info` instead, which caches its data. The returned module name is the fully qualified name, except in the case of `__main__`. """ # Three lines below work, even when chdir is called; just get __name__ from # the globals dict. calling_fun_globals = get_calling_fun_globals_dict(level=level+2) calling_module_name = calling_fun_globals["__name__"] calling_module = sys.modules[calling_module_name] # Two lines below work. They fail, though, when os.chdir("..") is called # before this function is called. #frame = inspect.stack()[level+1] #calling_module = inspect.getmodule(frame[0]) # Four lines below work, using sys rather than inspect. # Someone says this way works with pyinstaller, too, but I haven't tried it. # Apparently sys._getframe is available with a performance hit in PyPy, # but sys._current_frames is less widely available. #f = list(sys._current_frames().values())[0] #for i in range(level): f = f.f_back #calling_module_name = f.f_globals['__name__'] #calling_module = sys.modules[calling_module_name] return calling_module.__name__, calling_module # # Test this file when invoked as a script. # if __name__ == "__main__": # This guard is optional, but slightly more efficient. script_run("../../test", pytest_args="-v -s", modify_syspath=True)