Пример #1
0
def test_pylint_template(stub_context, tmp_path):
    def test_pylint_load():
        try:
            lint_args = ["--rcfile", str(expected_path.absolute())]
            pylint.lint.Run(lint_args)
        except SyntaxError:
            pytest.fail(str(SyntaxError))  # noqa
        except:  # noqa
            pass

    stubs, paths, ctx_paths = stub_context
    ctx_datadir = tmp_path / "ctx_cata"
    ctx_datadir.mkdir(exist_ok=True)
    prov = TemplateProvider(["pylint"])
    prov.render_to("pylint", tmp_path, stubs=stubs, paths=ctx_paths, datadir=ctx_datadir)
    expected_path = tmp_path / ".pylintrc"
    assert expected_path.exists()
    # Will Pylint load it?
    test_pylint_load()
    # Test Update
    new_path = tmp_path / ".micropy" / "foobar" / "foo"
    ctx_paths.append(new_path)
    prov.update("pylint", tmp_path, stubs=stubs, paths=ctx_paths, datadir=ctx_datadir)
    init_hook = expected_path.read_text().splitlines(True)[2]
    hook_imports = init_hook.split(",")
    hook_path = str(Path(".micropy/foobar/foo")).replace(
        "\\", "/"
    )  # no need to use \\ on pylint Windows
    assert f' "{hook_path}"' in hook_imports
    test_pylint_load()
Пример #2
0
def test_pylint_template(stub_context, tmp_path):
    def test_pylint_load():
        try:
            lint_args = ["--rcfile", str(expected_path.absolute())]
            pylint.lint.Run(lint_args)
        except SyntaxError:
            pytest.fail(str(SyntaxError))  # noqa
        except:  # noqa
            pass

    stubs, paths, ctx_paths = stub_context
    ctx_datadir = tmp_path / 'ctx_cata'
    ctx_datadir.mkdir(exist_ok=True)
    prov = TemplateProvider(['pylint'])
    prov.render_to("pylint",
                   tmp_path,
                   stubs=stubs,
                   paths=ctx_paths,
                   datadir=ctx_datadir)
    expected_path = tmp_path / '.pylintrc'
    assert expected_path.exists()
    # Will Pylint load it?
    test_pylint_load()
    # Test Update
    new_path = (tmp_path / '.micropy' / 'foobar' / 'foo')
    ctx_paths.append(new_path)
    prov.update("pylint",
                tmp_path,
                stubs=stubs,
                paths=ctx_paths,
                datadir=ctx_datadir)
    init_hook = expected_path.read_text().splitlines(True)[2]
    hook_imports = init_hook.split(";")
    assert 'sys.path.insert(1, ".micropy/foobar/foo")' in hook_imports
    test_pylint_load()
Пример #3
0
def test_vscode_template(stub_context, shared_datadir, tmp_path, mock_checks):
    stubs, paths, ctx_paths = stub_context
    prov = TemplateProvider(['vscode'])
    ctx_datadir = tmp_path / 'ctx_cata'
    ctx_datadir.mkdir(exist_ok=True)
    # Add test local path
    ctx_local = ctx_datadir / 'src' / 'lib' / 'somelib'
    ctx_local.mkdir(parents=True)
    ctx_absolute = Path('/fakedir/notinprojectdir/somelib')
    ctx_local_paths = [ctx_local, ctx_absolute]
    prov.render_to('vscode',
                   tmp_path,
                   stubs=stubs,
                   paths=ctx_paths,
                   datadir=ctx_datadir,
                   local_paths=ctx_local_paths)
    expected_path = tmp_path / '.vscode' / 'settings.json'
    out_content = expected_path.read_text()
    print(out_content)
    # Get rid of comments
    with expected_path.open() as f:
        lines = [l.strip() for l in f.readlines() if l]
        valid = [l for l in lines if "//" not in l[:2]]
    # Valid JSON?
    expect_paths = [str(p.relative_to(tmp_path)) for p in ctx_paths]
    expect_paths.append(str(ctx_local.relative_to(
        tmp_path)))  # add local path (should be relative)
    # local path outside of project dir (must be absolute)
    expect_paths.append(str(ctx_absolute.absolute()))
    content = json.loads("\n".join(valid))
    assert sorted(expect_paths) == sorted(
        content["python.autoComplete.extraPaths"])
    assert expected_path.exists()
    # Test Update
    ctx_paths.append((tmp_path / "foobar" / "foo.py"))
    prov.update('vscode',
                tmp_path,
                stubs=stubs,
                paths=ctx_paths,
                datadir=ctx_datadir,
                local_paths=ctx_local_paths)
    content = json.loads(expected_path.read_text())
    expect_paths.append(
        str((tmp_path / "foobar" / "foo.py").relative_to(tmp_path)))
    assert sorted(expect_paths) == sorted(
        content["python.autoComplete.extraPaths"])
    # Test update with missing file
    expected_path.unlink()  # delete file
    prov.update('vscode',
                tmp_path,
                stubs=stubs,
                paths=ctx_paths,
                datadir=ctx_datadir)
    assert expected_path.exists()
Пример #4
0
def test_generic_template(mock_mp_stubs, tmp_path):
    prov = TemplateProvider(['bootstrap', 'pymakr'])
    prov.render_to('boot', tmp_path)
    expected_path = tmp_path / 'src' / 'boot.py'
    assert expected_path.exists()
    expected_content = (prov.TEMPLATE_DIR / 'src' / 'boot.py').read_text()
    out_content = expected_path.read_text()
    print(out_content)
    assert expected_content.strip() == out_content.strip()
    templ = prov.get('boot')
    assert templ.update(tmp_path) is None
Пример #5
0
def test_generic_template(mock_mp_stubs, tmp_path):
    prov = TemplateProvider(["bootstrap", "pymakr"])
    prov.render_to("boot", tmp_path)
    expected_path = tmp_path / "src" / "boot.py"
    assert expected_path.exists()
    expected_content = (prov.TEMPLATE_DIR / "src" / "boot.py").read_text()
    out_content = expected_path.read_text()
    print(out_content)
    assert expected_content.strip() == out_content.strip()
    templ = prov.get("boot")
    assert templ.update(tmp_path) is None
Пример #6
0
def test_vscode_template(stub_context, shared_datadir, tmp_path, mock_checks):
    stubs, paths, ctx_paths = stub_context
    prov = TemplateProvider(['vscode'])
    ctx_datadir = tmp_path / 'ctx_cata'
    ctx_datadir.mkdir(exist_ok=True)
    prov.render_to('vscode',
                   tmp_path,
                   stubs=stubs,
                   paths=ctx_paths,
                   datadir=ctx_datadir)
    expected_path = tmp_path / '.vscode' / 'settings.json'
    out_content = expected_path.read_text()
    print(out_content)
    # Get rid of comments
    with expected_path.open() as f:
        lines = [l.strip() for l in f.readlines() if l]
        valid = [l for l in lines if "//" not in l[:2]]
    # Valid JSON?
    expect_paths = [str(p.relative_to(tmp_path)) for p in ctx_paths]
    content = json.loads("\n".join(valid))
    assert sorted(expect_paths) == sorted(
        content["python.autoComplete.extraPaths"])
    assert expected_path.exists()
    # Test Update
    ctx_paths.append((tmp_path / "foobar" / "foo.py"))
    prov.update('vscode',
                tmp_path,
                stubs=stubs,
                paths=ctx_paths,
                datadir=ctx_datadir)
    content = json.loads(expected_path.read_text())
    expect_paths.append(
        str((tmp_path / "foobar" / "foo.py").relative_to(tmp_path)))
    assert sorted(expect_paths) == sorted(
        content["python.autoComplete.extraPaths"])
    # Test update with missing file
    expected_path.unlink()  # delete file
    prov.update('vscode',
                tmp_path,
                stubs=stubs,
                paths=ctx_paths,
                datadir=ctx_datadir)
    assert expected_path.exists()
Пример #7
0
class TemplatesModule(ProjectModule):
    """Project Templates Module.

    Generates and manages project files using the Projects
    context.

    Args:
        templates (List[str]): List of templates to use.
        run_checks (bool, optional): Whether to execute checks or not.
            Defaults to True.

    """
    PRIORITY: int = 1
    TEMPLATES = TemplateProvider.TEMPLATES
    _dynamic = ['vscode', 'pylint']

    def __init__(self, templates=None, run_checks=True, **kwargs):
        self._templates = templates or []
        super().__init__(**kwargs)
        self.run_checks = run_checks

    @property
    def config(self):
        """Template config.

        Returns:
            dict: Current configuration

        """
        return self.parent.config

    def get_provider(self, templates):
        return TemplateProvider(templates,
                                run_checks=self.run_checks,
                                log=self.log)

    def load(self, **kwargs):
        """Loads project templates."""
        self.provider = self.get_provider(self.config.get('config'))
        templates = [k for k, v in self.config.get('config').items() if v]
        self.log.debug(f"Loading Templates: {templates}")
        self.provider = TemplateProvider(templates, **kwargs)
        self.update()

    def create(self):
        """Generates project files.

        Returns:
            dict: Project context

        """
        self.log.title("Rendering Templates")
        self.log.info("Populating Stub Info...")
        for key in self._templates:
            if key in self._dynamic:
                self.config.add('config' + '/' + key, True)
        self.provider = self.get_provider(self._templates)
        for t in self.provider.templates:
            self.provider.render_to(t, self.parent.path,
                                    **self.parent.context.raw())
        self.log.success("Stubs Injected!")
        return self._templates

    def update(self):
        """Updates project files.

        Returns:
            dict: Project context

        """
        self.provider = self.get_provider(self.config.get('config'))
        self.log.debug(
            f"updating templates with context: {self.parent.context.raw()}")
        for tmp in self.provider.templates:
            self.provider.update(tmp, self.parent.path,
                                 **self.parent.context.raw())
        return self.parent.context
Пример #8
0
class Project:
    """Handles Micropy Projects

    Args:
        path (str): Path to project
        name (str, optional): Name of project.
            Defaults to None. If none, path name is used.
        stubs (Stub, optional): List of Stubs to use.
            Defaults to None.
        stub_manager (StubManager, optional): StubManager to source stubs.
                Defaults to None.
    """

    TEMPLATES = TemplateProvider.TEMPLATES

    def __init__(self, path, name=None, templates=[], stubs=None,
                 stub_manager=None):
        self._loaded = False
        self.path = Path(path).absolute()
        self.data = self.path / '.micropy'
        self.cache = self.data / '.cache'
        self.info_path = self.path / 'micropy.json'
        self.stub_manager = stub_manager

        self.name = name or self.path.name
        self.stubs = stubs

        self.requirements = self.path / 'requirements.txt'
        self.dev_requirements = self.path / 'dev-requirements.txt'
        self.packages = {}
        self.dev_packages = {'micropy-cli': '*'}
        self.pkg_data = self.data / self.name

        self.config = {'vscode': False, 'pylint': False}
        self.log = Log.add_logger(self.name, show_title=False)
        template_log = Log.add_logger(
            "Templater", parent=self.log, show_title=False)
        self.provider = None
        if templates:
            for key in self.config:
                if key in templates:
                    self.config[key] = True
            self.provider = TemplateProvider(templates, log=template_log)

    def _set_cache(self, key, value):
        """Set key in Project cache

        Args:
            key (str): Key to set
            value (obj): Value to set
        """
        if not self.cache.exists():
            self.cache.write_text("{}")
        data = json.loads(self.cache.read_text())
        data[key] = value
        with self.cache.open('w+') as f:
            json.dump(data, f)

    def _get_cache(self, key):
        """Retrieve value from Project Cache

        Args:
            key (str): Key to retrieve

        Returns:
            obj: Value at key
        """
        if not self.cache.exists():
            return None
        data = json.loads(self.cache.read_text())
        value = data.pop(key, None)
        return value

    def _resolve_subresource(self, stubs):
        """Resolves stub resource

        Args:
            stubs (stubs): Stubs Passed to Manager
        """
        try:
            resource = set(
                self.stub_manager.resolve_subresource(stubs, self.data))
        except OSError as e:
            msg = "Failed to Create Stub Links!"
            exc = StubError(message=msg)
            self.log.error(str(e), exception=exc)
            sys.exit(1)
        else:
            return resource

    def _load_stubs(self, stubs):
        """Loads stubs from info file

        Args:
            stub_list (dict): Dict of Stubs
        """
        for name, location in stubs.items():
            _path = self.path / location
            if Path(_path).exists():
                yield self.stub_manager.add(_path)
            else:
                yield self.stub_manager.add(name)

    def _fetch_package(self, url):
        """Fetch and stub package at url

        Args:
            url (str): URL to fetch

        Returns:
            Path: path to package
        """
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_path = Path(tmp_dir)
            file_name = utils.get_url_filename(url)
            _file_name = "".join(self.log.iter_formatted(f"$B[{file_name}]"))
            content = utils.stream_download(
                url, desc=f"{self.log.get_service()} {_file_name}")
            pkg_path = utils.extract_tarbytes(content, tmp_path)
            ignore = ['setup.py', '__version__', 'test_']
            pkg_init = next(pkg_path.rglob("__init__.py"), None)
            py_files = [f for f in pkg_path.rglob(
                "*.py") if not any(i in f.name for i in ignore)]
            stubs = [utils.generate_stub(f) for f in py_files]
            if pkg_init:
                data_path = self.pkg_data / pkg_init.parent.name
                shutil.copytree(pkg_init.parent, data_path)
                return data_path
            for file, stub in stubs:
                shutil.copy2(file, (self.pkg_data / file.name))
                shutil.copy2(stub, (self.pkg_data / stub.name))

    def load_packages(self):
        """Retrieves and stubs project requirements"""
        pkg_keys = set(self.packages.keys())
        pkg_cache = self._get_cache('pkg_loaded')
        new_pkgs = pkg_keys.copy()
        if pkg_cache:
            new_pkgs = new_pkgs - set(pkg_cache)
        pkgs = [(name, s)
                for name, s in self.packages.items() if name in new_pkgs]
        if pkgs and not self._loaded:
            self.log.title("Fetching Requirements")
        for name, spec in pkgs:
            meta = utils.get_package_meta(name, spec=spec)
            tar_url = meta['url']
            self._fetch_package(tar_url)
        self.update_all()
        self._set_cache('pkg_loaded', list(pkg_keys))

    def load(self, verbose=True, **kwargs):
        """Load existing project

        Args:
            verbose (bool): Log to stdout. Defaults to True.

        Returns:
            stubs: Project Stubs
        """
        data = json.loads(self.info_path.read_text())
        _stubs = data.get("stubs")
        self.name = data.get("name", self.name)
        self.config = data.get("config", self.config)
        templates = [k for k, v in self.config.items() if v]
        self.provider = TemplateProvider(templates)
        self.packages = data.get("packages", self.packages)
        self.dev_packages = data.get("dev-packages", self.dev_packages)
        self.stubs = kwargs.get('stubs', self.stubs)
        self.stub_manager = kwargs.get("stub_manager", self.stub_manager)
        self.stub_manager.verbose_log(verbose)
        self.data.mkdir(exist_ok=True)
        stubs = list(self._load_stubs(_stubs))
        if self.stubs:
            stubs.extend(self.stubs)
        self.stubs = self._resolve_subresource(stubs)
        self.pkg_data.mkdir(exist_ok=True)
        self.load_packages()
        self._loaded = True
        if verbose:
            self.log.success(f"\nProject Ready!")
        return self.stubs

    def add_stub(self, stub, **kwargs):
        """Add stub to project

        Args:
            stub (Stub): Stub object to add

        Returns:
            [Stubs]: Project Stubs
        """
        loaded = self.stubs or []
        stubs = [*loaded, stub]
        self.log.info("Loading project...")
        self.load(stubs=stubs, verbose=False, **kwargs)
        self.log.info("Updating Project Info...")
        self.to_json()
        self.log.info(
            f"Project Stubs: $[{' '.join(str(s) for s in self.stubs)}]")
        self.log.success("\nProject Updated!")
        return self.stubs

    def add_package(self, package, dev=False):
        """Add requirement to project

        Args:
            package (str): package name/spec
            dev (bool, optional): Flag requirement as dev. Defaults to False.

        Returns:
            dict: Dictionary of packages
        """
        pkg = next(requirements.parse(package))
        self.log.info(f"Adding $[{pkg.name}] to requirements...")
        packages = self.dev_packages if dev else self.packages
        if packages.get(pkg.name, None):
            self.log.error(f"$[{package}] is already installed!")
            return None
        specs = "".join(next(iter(pkg.specs))) if pkg.specs else "*"
        packages[pkg.name] = specs
        self.to_json()
        self.load_packages()
        self.log.success("Package installed!")
        return packages

    def _add_pkgs_from_file(self, path, **kwargs):
        """Add packages listed in a file

        Args:
            path (str): path to file

        Returns:
            list of added reqs
        """
        if not path.exists():
            return []
        reqs = list(utils.iter_requirements(path))
        for req in reqs:
            self.add_package(req.line, **kwargs)
        return reqs

    def add_from_requirements(self):
        """Add all packages in requirements.txt files

        Returns:
            List of all added requirements
        """
        reqs = self._add_pkgs_from_file(self.requirements)
        dev_reqs = self._add_pkgs_from_file(self.dev_requirements, dev=True)
        all_reqs = [*reqs, *dev_reqs]
        return all_reqs if all_reqs else None

    def exists(self):
        """Whether this project exists

        Returns:
            bool: True if it exists
        """
        return self.info_path.exists()

    @property
    def context(self):
        """Get project template context"""
        paths = []
        if self.stubs:
            frozen = [s.frozen for s in self.stubs]
            fware_mods = [s.firmware.frozen
                          for s in self.stubs if s.firmware is not None]
            stub_paths = [s.stubs for s in self.stubs]
            paths = [*fware_mods, *frozen, *stub_paths]
            if self.pkg_data.exists():
                paths.append(self.pkg_data)
        return {
            "stubs": self.stubs,
            "paths": paths,
            "datadir": self.data,
        }

    @property
    def info(self):
        """Project Information"""
        stubs = {s.name: s.stub_version for s in self.stubs}
        return {
            "name": self.name,
            "stubs": stubs,
            "config": self.config,
            "packages": self.packages,
            "dev-packages": self.dev_packages
        }

    def _dump_requirements(self, packages, path):
        """Dumps packages to file at path

        Args:
            packages (dict): dict of packages to dump
            path (str): path to fiel
        """
        if not path.exists():
            path.touch()
        pkgs = [(f"{name}{spec}" if spec and spec != "*" else name)
                for name, spec in packages.items()]
        with path.open('r+') as f:
            content = [c.strip() for c in f.readlines() if c.strip() != '']
            _lines = sorted(set(pkgs) | set(content))
            lines = [l + "\n" for l in _lines]
            f.seek(0)
            f.writelines(lines)

    def to_requirements(self):
        """Dumps requirements to .txt files"""
        self._dump_requirements(self.packages, self.requirements)
        self._dump_requirements(self.dev_packages, self.dev_requirements)

    def to_json(self):
        """Dumps project to data file"""
        with self.info_path.open('w+') as f:
            data = json.dumps(self.info, indent=4)
            f.write(data)

    def render_all(self):
        """Renders all project files"""
        self.log.info("Populating Stub info...")
        for t in self.provider.templates:
            self.provider.render_to(t, self.path, **self.context)
        self.log.success("Stubs Injected!")
        return self.context

    def update_all(self):
        """Updates all project files"""
        self.to_requirements()
        for t in self.provider.templates:
            self.provider.update(t, self.path, **self.context)
        return self.context

    def create(self):
        """creates a new project"""
        self.log.title(f"Initiating $[{self.name}]")
        self.data.mkdir(exist_ok=True, parents=True)
        self.stubs = self._resolve_subresource(self.stubs)
        self.log.info(
            f"Stubs: $[{' '.join(str(s) for s in self.stubs)}]")
        self.log.debug(f"Generated Project Context: {self.context}")
        if self.provider:
            self.log.title("Rendering Templates")
            self.render_all()
        self.update_all()
        self.to_json()
        self.log.success(f"Project Created!")
        return self.path.relative_to(Path.cwd())

    @classmethod
    def resolve(cls, path, verbose=True):
        """Returns project from path if it exists

        Args:
            path (str): Path to test
            verbose (bool): Log to stdout. Defaults to True.

        Returns:
            (Project|None): Project if it exists
        """
        path = Path(path).resolve()
        proj = cls(path)
        if proj.exists():
            micropy = MicroPy()
            if verbose:
                micropy.log.title(f"Loading Project")
            proj.load(stub_manager=micropy.STUBS, verbose=verbose)
            return proj
Пример #9
0
class TemplatesModule(ProjectModule):
    """Project Templates Module.

    Generates and manages project files using the Projects
    context.

    Args:
        templates (List[str]): List of templates to use.
        run_checks (bool, optional): Whether to execute checks or not.
            Defaults to True.

    """
    TEMPLATES = TemplateProvider.TEMPLATES

    def __init__(self, templates=None, run_checks=True, **kwargs):
        self.templates = templates or []
        self.run_checks = run_checks
        self.enabled = {'vscode': False, 'pylint': False}
        self.log = Log.add_logger('Templater', show_title=False)
        if templates:
            for key in self.enabled:
                if key in self.templates:
                    self.enabled[key] = True
        self.provider = TemplateProvider(self.templates,
                                         run_checks=self.run_checks,
                                         log=self.log,
                                         **kwargs)

    @property
    def config(self):
        """Template config.

        Returns:
            dict: Current configuration

        """
        _config = self.parent.config.get('config', {})
        self.enabled = {**self.enabled, **_config}
        return {'config': self.enabled}

    def load(self, **kwargs):
        """Loads project templates."""
        _data = self.config.get('config')
        self.enabled = {**self.enabled, **_data}
        templates = [k for k, v in self.enabled.items() if v]
        self.log.debug(f"Loading Templates: {templates}")
        self.provider = TemplateProvider(templates, **kwargs)
        self.update()

    def create(self):
        """Generates project files.

        Returns:
            dict: Project context

        """
        self.log.title("Rendering Templates")
        self.log.info("Populating Stub Info...")
        for t in self.provider.templates:
            self.provider.render_to(t, self.parent.path,
                                    **self.parent.context.raw)
        self.log.success("Stubs Injected!")
        return self.parent.context

    def update(self):
        """Updates project files.

        Returns:
            dict: Project context

        """
        for tmp in self.provider.templates:
            self.provider.update(tmp, self.parent.path,
                                 **self.parent.context.raw)
        return self.parent.context