Skip to content

Commit

Permalink
[DjangoSettings] Scan python files
Browse files Browse the repository at this point in the history
Scan settings.py files searching for some Django specific entries where dependencies can hide in.
  • Loading branch information
gforcada committed Dec 13, 2017
1 parent fa658d8 commit bbfb831
Show file tree
Hide file tree
Showing 2 changed files with 220 additions and 0 deletions.
71 changes: 71 additions & 0 deletions z3c/dependencychecker/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from xml.etree import ElementTree
from z3c.dependencychecker.dotted_name import DottedName
import ast
import fnmatch
import os
import re

Expand Down Expand Up @@ -366,11 +367,81 @@ def scan(self):
yield dotted_name


class DjangoSettings(PythonModule):
"""Extract imports from Django settings.py
These files are used to enable Django components.
"""

@classmethod
def search_files(cls, top_dir):
"""Find all settings.py files in the package
For that it gets the path to where top_level.txt points to,
which is not always a folder:
- folder example: z3c.dependencychecker itself
- file example: flake8-isort or any other single file distribution
Return this very same class, which would allow to call the scan()
method to get an iterator over all this file's imports.
"""
if top_dir.endswith('.py'):
return

for path, folders, filenames in os.walk(top_dir):
for filename in filenames:
if fnmatch.fnmatch(filename, '*settings.py'):
yield cls(
top_dir,
os.path.join(path, filename),
)

def scan(self):
for node in ast.walk(self._get_tree()):
if isinstance(node, ast.Assign):
if self._is_installed_apps_assignment(node):
if isinstance(node.value, (ast.Tuple, ast.List)):
for element in node.value.elts:
if isinstance(element, ast.Str):
yield DottedName(
element.s,
file_path=self.path,
is_test=self.testing,
)

if self._is_test_runner_assignment(node):
if isinstance(node.value, ast.Str):
yield DottedName(
node.value.s,
file_path=self.path,
is_test=True,
)

@staticmethod
def _is_installed_apps_assignment(node):
if len(node.targets) == 1 and \
isinstance(node.targets[0], ast.Name) and \
node.targets[0].id == 'INSTALLED_APPS':
return True

return False

@staticmethod
def _is_test_runner_assignment(node):
if len(node.targets) == 1 and \
isinstance(node.targets[0], ast.Name) and \
node.targets[0].id == 'TEST_RUNNER':
return True

return False


MODULES = (
PythonModule,
ZCMLFile,
FTIFile,
GSMetadata,
PythonDocstrings,
DocFiles,
DjangoSettings,
)
149 changes: 149 additions & 0 deletions z3c/dependencychecker/tests/test_modules_django_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
from z3c.dependencychecker.modules import DjangoSettings
from z3c.dependencychecker.tests.utils import write_source_file_at
import os
import tempfile


RANDOM_CODE = 'from zope.component import adapter'
RANDOM_ASSIGNMENT = 'a = 3'
APPS_ASSIGNMENT_TO_STRING = 'INSTALLED_APPS = "random"'
APPS_ASSIGNMENT_TO_TUPLE = 'INSTALLED_APPS = ("random1", "random2", )'
APPS_ASSIGNMENT_TO_LIST = 'INSTALLED_APPS = ["random3", "random4", ]'
APPS_ASSIGNMENT_TO_LIST_MIXED = 'INSTALLED_APPS = ["random5", 3, ]'
TEST_RUNNER_ASSIGNMENT_TO_LIST = 'TEST_RUNNER = ["random6", "random7", ]'
TEST_RUNNER_ASSIGNMENT_TO_STRING = 'TEST_RUNNER = "random8"'


def _get_imports_of_python_module(folder, source):
temporal_file = write_source_file_at(
(folder.strpath, ),
source_code=source,
)

django_settings = DjangoSettings(folder.strpath, temporal_file)
dotted_names = [x.name for x in django_settings.scan()]
return dotted_names


def test_search_files_nothing(minimal_structure):
path, package_name = minimal_structure
modules_found = [x for x in DjangoSettings.search_files(path)]
assert len(modules_found) == 0


def test_search_files_single_file_random_name():
_, tmp_file = tempfile.mkstemp('.py')
modules_found = [x for x in DjangoSettings.search_files(tmp_file)]
assert len(modules_found) == 0


def test_search_files_single_file_settings_name(minimal_structure):
path, package_name = minimal_structure
src_path = os.path.join(path, 'src')
write_source_file_at([src_path, ], filename='some_settings.py', )
modules_found = [x for x in DjangoSettings.search_files(src_path)]
assert len(modules_found) == 1


def test_search_files_deep_nested(minimal_structure):
path, package_name = minimal_structure
src_path = os.path.join(path, 'src')
write_source_file_at(
[src_path, 'a', 'b', 'c', ],
filename='anothersettings.py',
)
modules_found = [x for x in DjangoSettings.search_files(src_path)]
assert len(modules_found) == 1


def test_random_code(tmpdir):
dotted_names = _get_imports_of_python_module(tmpdir, RANDOM_CODE)
assert len(dotted_names) == 0


def test_random_assignment(tmpdir):
dotted_names = _get_imports_of_python_module(tmpdir, RANDOM_ASSIGNMENT)
assert len(dotted_names) == 0


def test_apps_assignment_to_string(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
APPS_ASSIGNMENT_TO_STRING,
)
assert len(dotted_names) == 0


def test_apps_assignment_to_tuple(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
APPS_ASSIGNMENT_TO_TUPLE,
)
assert len(dotted_names) == 2


def test_apps_assignment_to_tuple_details(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
APPS_ASSIGNMENT_TO_TUPLE,
)
assert 'random1' in dotted_names
assert 'random2' in dotted_names


def test_apps_assignment_to_list(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
APPS_ASSIGNMENT_TO_LIST,
)
assert len(dotted_names) == 2


def test_apps_assignment_to_list_details(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
APPS_ASSIGNMENT_TO_LIST,
)
assert 'random3' in dotted_names
assert 'random4' in dotted_names


def test_apps_assignment_to_list_mixed(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
APPS_ASSIGNMENT_TO_LIST_MIXED,
)
assert len(dotted_names) == 1


def test_apps_assignment_to_list_mixed_details(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
APPS_ASSIGNMENT_TO_LIST_MIXED,
)
assert dotted_names[0] == 'random5'


def test_runner_assignment_to_list(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
TEST_RUNNER_ASSIGNMENT_TO_LIST,
)
assert len(dotted_names) == 0


def test_runner_assignment_to_string(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
TEST_RUNNER_ASSIGNMENT_TO_STRING,
)
assert len(dotted_names) == 1


def test_apps_assignment_to_string_mixed_details(tmpdir):
dotted_names = _get_imports_of_python_module(
tmpdir,
TEST_RUNNER_ASSIGNMENT_TO_STRING,
)
assert dotted_names[0] == 'random8'

0 comments on commit bbfb831

Please sign in to comment.