Scanning modules
================

Martian can grok modules or packages. In order to grok packages, it
needs to scan all modules in it. The martian.scan package provides an
abstraction over packages and modules that helps with the scanning
process.

  >>> from martian.scan import module_info_from_dotted_name
  
We have provided a special test fixture package called stoneage that we are
going to scan, in ``martian.tests.stoneage``.

Modules
-------

The scanning module defines a class ``ModuleInfo`` that provides
information about a module or a package. Let's take a look at the
``cave`` module in the stone-age package::

  >>> module_info = module_info_from_dotted_name('martian.tests.stoneage.cave')
  
We get a ``ModuleInfo`` object representing the ``cave module::

  >>> module_info
  <ModuleInfo object for 'martian.tests.stoneage.cave'>
  
``cave`` is a module, not a package.

  >>> module_info.isPackage()
  False

We can retrieve the name of the module::

  >>> module_info.name
  'cave'

We can also retrieve the dotted name of the module::

  >>> module_info.dotted_name
  'martian.tests.stoneage.cave'

And the dotted name of the package the module is in::

  >>> module_info.package_dotted_name
  'martian.tests.stoneage'

It is possible to get the actual module object that the ModuleInfo
object stands for, in this case the package's ``cave.py``::

  >>> module = module_info.getModule()
  >>> module
  <module 'martian.tests.stoneage.cave' from '...cave.py...'>

We can store a module-level annotation in the module::

  >>> module.__grok_foobar__ = 'GROK LOVE FOO'

The ModuleInfo object allows us to retrieve the annotation again::

  >>> module_info.getAnnotation('grok.foobar', None)
  'GROK LOVE FOO'

If a requested annotation does not exist, we get the default value::

  >>> module_info.getAnnotation('grok.barfoo', 42)
  42

A module has no sub-modules in it (only packages have this)::

  >>> module_info.getSubModuleInfos()
  []

Trying to retrieve any sub modules will give back None::

  >>> print module_info.getSubModuleInfo('doesnotexist')
  None

Packages
--------

Now let's scan a package::

  >>> module_info = module_info_from_dotted_name('martian.tests.stoneage')

We will get a ModuleInfo instance representing the ``stoneage`` package::

  >>> module_info
  <ModuleInfo object for 'martian.tests.stoneage'>

The object knows it is a package::

  >>> module_info.isPackage()
  True

Like with the module, we can get the package's name::

  >>> module_info.name
  'stoneage'

We can also get the package's dotted name back from it::

  >>> module_info.dotted_name
  'martian.tests.stoneage'

It is also possible to get the dotted name of the nearest package the
package resides in. This will always be itself::

  >>> module_info.package_dotted_name
  'martian.tests.stoneage'

Now let's go into the package and a few sub modules that are in it::

  >>> module_info.getSubModuleInfo('cave')
  <ModuleInfo object for 'martian.tests.stoneage.cave'>

  >>> module_info.getSubModuleInfo('hunt')
  <ModuleInfo object for 'martian.tests.stoneage.hunt'>

Trying to retrieve non-existing sub modules gives back None::

  >>> print module_info.getSubModuleInfo('doesnotexist')
  None

It is possible to get the actual module object that the ModuleInfo
object stands for, in this case the package's ``__init__.py``::

  >>> module = module_info.getModule()
  >>> module
  <module 'martian.tests.stoneage' from '...__init__.py...'>

A package has sub modules::

  >>> sub_modules = module_info.getSubModuleInfos()
  >>> sub_modules
  [<ModuleInfo object for 'martian.tests.stoneage.cave'>,
   <ModuleInfo object for 'martian.tests.stoneage.hunt'>,
   <ModuleInfo object for 'martian.tests.stoneage.painting'>]

Resource paths
--------------

Resources can be stored in a directory alongside a module (in their
containing package).  We can get the path to such a resource directory
using the ``getResourcePath`` method.

For packages, a resource path will be a child of the package directory:

  >>> import os.path
  >>> expected_resource_path = os.path.join(os.path.dirname(
  ...     module.__file__), 'stoneage-templates')
  >>> resource_path = module_info.getResourcePath('stoneage-templates')
  >>> resource_path == expected_resource_path
  True

For modules, a resource path will be a sibling of the module's file:

  >>> cave_module_info = module_info_from_dotted_name(
  ...    'martian.tests.stoneage.cave')
  >>> expected_resource_path = os.path.join(os.path.dirname(
  ...     cave_module_info.getModule().__file__), 'cave-templates')
  >>> resource_path = cave_module_info.getResourcePath('cave-templates')
  >>> resource_path == expected_resource_path
  True

Skipping test packages and modules
----------------------------------

By default functional tests and unit tests are skipped from the grokking
procedure.

Packages called 'tests' (the "de facto" standard name for packages containing
unit tests) or 'ftests' (for functional tests) are skipped (and thus not
grokked)::

  >>> from martian.scan import ModuleInfo, module_info_from_dotted_name
  >>> module_info = module_info_from_dotted_name(
  ...     'martian.tests.withtestspackages')
  >>> module_info
  <ModuleInfo object for 'martian.tests.withtestspackages'>
  >>> # Will *not* contain the module info for the tests and ftests packages
  >>> print module_info.getSubModuleInfos()
  [<ModuleInfo object for 'martian.tests.withtestspackages.subpackage'>]

Likewise modules called tests.py or ftests.py are skipped::

  >>> from martian.scan import ModuleInfo, module_info_from_dotted_name
  >>> module_info = module_info_from_dotted_name(
  ...     'martian.tests.withtestsmodules')
  >>> module_info
  <ModuleInfo object for 'martian.tests.withtestsmodules'>
  >>> # Will *not* contain the module info for the tests and ftests modules
  >>> print module_info.getSubModuleInfos()
  [<ModuleInfo object for 'martian.tests.withtestsmodules.subpackage'>]

You can still get to the module info of tests and ftests if you need to::

  >>> module_info = module_info_from_dotted_name(
  ...     'martian.tests.withtestspackages')
  >>> module_info
  <ModuleInfo object for 'martian.tests.withtestspackages'>
  >>> print module_info.getSubModuleInfos(exclude_tests=False)
  [<ModuleInfo object for 'martian.tests.withtestspackages.ftests'>,
  <ModuleInfo object for 'martian.tests.withtestspackages.subpackage'>,
  <ModuleInfo object for 'martian.tests.withtestspackages.tests'>]

You can also explicitely grok tests and ftests packages::

  >>> module_info = module_info_from_dotted_name(
  ...     'martian.tests.withtestspackages.tests')
  >>> module_info
  <ModuleInfo object for 'martian.tests.withtestspackages.tests'>
  >>> print module_info.getSubModuleInfos()
  [<ModuleInfo object for 'martian.tests.withtestspackages.tests.subpackage'>]
