Skip to content

Commit

Permalink
feat(skm): pytlas version check (#28)
Browse files Browse the repository at this point in the history
* feat: Add __pytlas_package__ to registered handler to keep a link to the package which registered this handler
* feat: Add pytlas skill compatibility check as per #27
* refacto: Add __about__ which contains metadata about pytlas
  • Loading branch information
YuukanOO committed Aug 28, 2019
1 parent 18f80c3 commit 3be1829
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 63 deletions.
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
include README.rst
include pytlas/version.py
include pytlas/__about__.py
15 changes: 8 additions & 7 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,20 @@
import sys
import os

with open('../pytlas/version.py') as f:
__version__ = f.readlines()[1].strip()[15:-1]
with open('../pytlas/__about__.py') as f:
ABOUT = {}
exec(f.read(), ABOUT)

# -- Project information -----------------------------------------------------

project = 'pytlas'
copyright = '2018, Julien LEICHER'
author = 'Julien LEICHER'
project = ABOUT['__title__']
copyright = '2018, %s' % ABOUT['__author__']
author = ABOUT['__author__']

# The short X.Y version
version = __version__
version = ABOUT['__version__']
# The full version, including alpha/beta/rc tags
release = __version__
release = ABOUT['__version__']


# -- General configuration ---------------------------------------------------
Expand Down
15 changes: 15 additions & 0 deletions pytlas/__about__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Some metadata about the pytlas library!
"""

__title__ = 'pytlas'
__summary__ = 'An open-source Python 3 assistant library built for people and '\
'made to be super easy to setup and understand!'
__author__ = 'Julien LEICHER'
__email__ = 'julien@leicher.me'
__license__ = 'GPL-3.0'

__version__ = '5.0.0'

__doc_url__ = 'https://pytlas.readthedocs.io/'
__tracker_url__ = 'https://github.com/atlassistant/pytlas/issues'
__github_url__ = 'https://github.com/atlassistant/pytlas'
2 changes: 1 addition & 1 deletion pytlas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
handlers.
"""

from pytlas.version import __version__
from pytlas.__about__ import __version__
from pytlas.conversing import Agent
from pytlas.handling import Card, intent, meta, translations
from pytlas.understanding import training
2 changes: 1 addition & 1 deletion pytlas/cli/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import cmd
import sys
from pytlas.version import __version__
from pytlas.__about__ import __version__


class Prompt(cmd.Cmd): # pragma: no cover
Expand Down
4 changes: 3 additions & 1 deletion pytlas/handling/skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def __init__(self, name=None, description='No description provided',
self.homepage = homepage
self.settings = [setting if isinstance(
setting, Setting) else Setting.from_value(setting) for setting in settings]
self.package = None # Represents the module which defines this meta
self.package = name # Represents the module which defines this meta

# pylint: enable=W0102,R0913

Expand Down Expand Up @@ -248,6 +248,8 @@ def register(self, intent_name, func, package=None):
package = package or get_caller_package_name() or func.__module__

self._data[intent_name] = func
# Let's add the package to the func to tied together each registered resources
func.__pytlas_package__ = package
self._logger.info('Registered "%s.%s" which should handle "%s" intent',
package, func.__name__, intent_name)

Expand Down
46 changes: 40 additions & 6 deletions pytlas/supporting/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import re
import os
import subprocess
from pkg_resources import Requirement
from packaging.version import Version
from packaging.specifiers import SpecifierSet
from pytlas.__about__ import __title__, __version__
from pytlas.handling.skill import GLOBAL_METAS, GLOBAL_HANDLERS, Meta
from pytlas.pkgutils import get_package_name_from_module
from pytlas.ioutils import rmtree
Expand Down Expand Up @@ -149,6 +153,15 @@ def path_to_skill_folder(path):
return path


class CompatibilityError(Exception):
"""Represents an exception when a skill could not work with the running
pytlas environment.
"""

def __init__(self, current_version, expected_specifications):
super().__init__(f'Current pytlas version "{current_version}" does not '\
f'satisfy skill dependency "{expected_specifications}"')

class SkillsManager:
"""The SkillsManager handles skill installation, updates, listing and removal.
It can be used with the built-in CLI or used as a library.
Expand All @@ -174,6 +187,7 @@ def __init__(self, directory, lang='en', default_git_url='https://github.com/',
self._default_git_url = default_git_url
self._metas = metas_store or GLOBAL_METAS
self._handlers = handlers_store or GLOBAL_HANDLERS
self._version = Version(__version__)

# pylint: enable=R0913

Expand All @@ -185,7 +199,7 @@ def get(self):
list of Meta: Skills loaded.
"""
unique_pkgs = list(set(get_package_name_from_module(
unique_pkgs = list(set(v.__pytlas_package__ or get_package_name_from_module(
v.__module__) for v in self._handlers._data.values())) # pylint: disable=W0212
self._logger.info(
'Retrieving meta for "%d" unique packages', len(unique_pkgs))
Expand Down Expand Up @@ -240,11 +254,10 @@ def install(self, *names):

succeeded.append(display_name)
logging.info('Successfully installed "%s"', repo)
except subprocess.CalledProcessError as err:
except (subprocess.CalledProcessError, CompatibilityError) as err:
failed.append(display_name)
self._logger.error(
'Could not clone the skill repo, make sure you didn\'t mispelled it and '
'you have sufficient rights to clone it. "%s"', err)
'Could not install skill "%s": %s', display_name, err)
rmtree(dest, ignore_errors=True) # try to clean up the directory

return (succeeded, failed)
Expand Down Expand Up @@ -285,10 +298,11 @@ def update(self, *names):
succeeded.append(display_name)
self._logger.info(
'Successfully updated "%s"', display_name)
except subprocess.CalledProcessError as err:
except (subprocess.CalledProcessError, CompatibilityError) as err:
failed.append(display_name)
self._logger.error(
'Could not pull in "%s": "%s"', folder, err)
'Could not update skill "%s" in "%s": %s',
display_name, folder, err)

return (succeeded, failed)

Expand Down Expand Up @@ -334,10 +348,30 @@ def _install_dependencies(self, directory):
requirements_path = os.path.join(directory, 'requirements.txt')

if os.path.isfile(requirements_path):
self._ensure_compatibility(requirements_path)
self._logger.info(
'Installing dependencies from "%s"', requirements_path)
subprocess.check_output(['pip', 'install', '-r', 'requirements.txt'],
stderr=subprocess.STDOUT, cwd=directory)
else:
self._logger.info(
'No requirements.txt available inside "%s", skipping', directory)

def _ensure_compatibility(self, requirements_path):
with open(requirements_path, encoding='utf8') as reqs_file:
def try_parse_requirement(line):
try:
return Requirement.parse(line)
except: # pylint: disable=W0702
return None

requirements = [try_parse_requirement(line) for line in reqs_file.readlines()]
pytlas_requirement = next((r for r in requirements\
if r and r.project_name == __title__),
None)

if pytlas_requirement:
specs_set = SpecifierSet(','.join(''.join(s) for s in pytlas_requirement.specs))

if self._version not in specs_set:
raise CompatibilityError(__version__, pytlas_requirement)
2 changes: 0 additions & 2 deletions pytlas/version.py

This file was deleted.

24 changes: 15 additions & 9 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@
with open('README.rst', encoding='utf-8') as f:
README = f.read()

with open('pytlas/version.py') as f:
VERSION = f.readlines()[1].strip()[15:-1]
with open('pytlas/__about__.py') as about_file:
ABOUT = {}
exec(about_file.read(), ABOUT) # pylint: disable=W0122

setup(
name='pytlas',
version=VERSION,
description='An open-source Python 3 assistant library built for people and '\
'made to be super easy to setup and understand!',
name=ABOUT['__title__'],
version=ABOUT['__version__'],
description=ABOUT['__summary__'],
long_description=README,
url='https://github.com/atlassistant/pytlas',
author='Julien LEICHER',
license='GPL-3.0',
url=ABOUT['__github_url__'],
project_urls={
"Documentation": ABOUT['__doc_url__'],
"Source": ABOUT['__github_url__'],
"Tracker": ABOUT['__tracker_url__'],
},
author=ABOUT['__author__'],
author_email=ABOUT['__email__'],
license=ABOUT['__license__'],
packages=find_packages(),
include_package_data=True,
classifiers=[
Expand Down
3 changes: 2 additions & 1 deletion tests/handling/test_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ def test_it_should_register_handlers_correctly(self):
def on_lights_on():
pass

s.register('lights_on', on_lights_on)
s.register('lights_on', on_lights_on, package='mypackage')

expect(s._data).to.equal({
'lights_on': on_lights_on,
})
expect(on_lights_on.__pytlas_package__).to.equal('mypackage')

def test_it_should_register_with_the_decorator_in_the_provided_store(self):
s = HandlersStore()
Expand Down

0 comments on commit 3be1829

Please sign in to comment.