Skip to content

[WIP] Feature Dependency Management #501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from

Conversation

mauicv
Copy link
Contributor

@mauicv mauicv commented May 11, 2022

TODO:

  • feature branch for tensorflow, tensorflow-probability and pytorch optional dependencies
  • feature branch for numba optional dependencies
  • feature branch for prophet optional dependencies
  • Image branch (see notion)

Outstanding questions:

  • The prophet optional dependency in detect currently has two dependencies: pystan and PyMeeus (via fbprophet) both of which are under "GNU General Public License v3 (GPLv3)" license. They weren't coming up with prior license checks because the script that checks the licenses uses extras=all in the tox-env settings but there is no all optional dependency option by default for pip installs instead it just gives a warning which I think is silenced in CI. Now that I've added all for the optional dependencies work, they've turned up. These are optional dependencies so this isn't really an issue but it raises the question of how we should be checking the licenses? i.e. do we look at all the dependencies or just the defaults? If we check all we should flag those that are optional so we know how much of an issue it is, etc... For now, I've set the license checks to only run on the default dependencies.

What is this PR?

This PR addresses optional dependency import functionality for Alibi Detect. The core behaviour this adds is to throw errors informing the user if they haven't installed the necessary optional dependency for the functionality they wish to use. These errors are thrown at use time rather than at import.

There are three behaviours in general:

  1. Optional dependency in a public object. In this case, we might have an object that can optionally use an optional dependency.
    • In this case, if the object is used in a way that doesn't use this dependency no error should be thrown.
    • If The object is used such that the optional dependency is used then the error should be thrown.
  2. Conditionally Optional dependency in an object. As an example, the value backend should be one of 'tensorflow' or 'torch' but not neither.
    • An error should be thrown if the user requests a backend that isn't installed.
  3. Objects that are completely dependent on an optional dependency
    • If the user imports and uses these then an error should be thrown

This PR addresses behaviours 1 and 3 above.


Note:

In order to manually test alibi-detect in each of the different environments developers can use:

make repl tox-env=<env>

and this will give them a REPL with the <env> optional dependencies installed.

For example: make repl tox-env=torch will give them the torch optional dependency environment REPL.


The main changes are:

  1. The MissingDependency and optional_import functionality,
  2. The refactoring of the codebase
  3. The tests.

1. MissingDependency

The MissingDependency metaclass is used to allow imports that have missing dependencies. If these imports are used later they will throw an error.

Implementation

The basic pattern is to replace constructs that require uninstalled dependencies with something like:

class MissingDependency:
    def __init__(self, missing_dependency: str, object_name: str):
        self.object_name = object_name
        self.missing_dependency = missing_dependency

    def __getattr__(self, key):
        raise ImportError()

    def __call__(self, *args, **kwargs):
        raise ImportError()

We also use an import function which should be used throughout by developers when importing constructs dependent on optional dependencies in order to ensure consistent behaviour. This looks roughly like:

def import_optional(module_name: str):
    try:
        return import_module(module_name)
    except (ImportError, ModuleNotFoundError) as err:
        return MissingDependency(
            missing_dependency=err.name, 
            object_name=module_name)

The above is called with:

UsefulClass = import_optional('alibi_detect.utils.useful_class')

The above raises an error if the user attempts to access an attribute or call the object.

  1. The error message informs the user that the UsefulClass object is missing optional dependencies.
  2. It tells the user how to resolve using pip install alibi-detect[optional-dependency].
  3. And finally it links to the original error thrown.

2. Refactoring:

The idea here is wherever the is an object that is dependent on an optional install the relevant functionality should be fenced off into a single file or collection of files. Access to that object should be via the __init__.py file. Within the __init__.py we will implement the MissingDependency pattern mentioned above.

Note on Type checking issue:

  • The import_optional function returns an object instance rather than an object. This will cause typechecking to fail
    if not all optional dependencies are installed. Because of this we also need to 1. Conditionally import the true object
    dependent on TYPE_CHECKING and 2. Use forward referencing within typing constructs such as Union. We use forward
    referencing because in a user environment the optional dependency may not be installed in which case it'll be replaced
    with an instance of the MissingDependency class. This will throw an error when passed to Union. See CONTRIBUTING.md note for more details and example.

3. Tests:

The tests import all the named objects from the public API of alibi and test that they throw the correct errors if the relevant optional dependencies are not installed. If these tests fail, it is likely that:

  1. The optional dependency relation hasn't been added to the test script. In this case, this test assumes that your
    functionality should work for the default alibi-detect install. If this is not the case you should add the exported object name to the dependency_map in the relevant test.
  2. The relevant export in the public API hasn't been protected with the MissingDependency class. In this case, see the docs string for the utils.missing_dependency m1odule.

Notes:

  1. The tests will be skipped in the normal test suite. To run correctly use tox. This should be as simple as running tox from the terminal. The environments will take a long time to build the first time but afterwards, this should be a quick process.
  2. If you need to configure a new optional dependency you will need to update the setup.cfg file and add a testenv environment as well as to the extra-requires.txt file
  3. Backend functionality may be unique to specific explainers/functions and so there may be multiple such modules that need to be tested separately.
  4. We assume all public functionality is exposed in modules via the __init__.py files.
  5. We assume all imports are top-level, if an import is nested within an object or function call this functionality will avoid being caught by the tests.

See also Further Notes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add saving and loading functionality for PyTorch backend
1 participant