Skip to content
This repository has been archived by the owner on Jan 12, 2021. It is now read-only.

Commit

Permalink
Merge branch 'universal-python-choice'
Browse files Browse the repository at this point in the history
  • Loading branch information
orsinium committed Apr 6, 2019
2 parents 59d6a93 + 8477f50 commit 6e1d8b4
Show file tree
Hide file tree
Showing 53 changed files with 698 additions and 415 deletions.
26 changes: 26 additions & 0 deletions dephell/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Actions are functions that used only in commands
"""

from ._autocomplete import make_bash_autocomplete, make_zsh_autocomplete
from ._converting import attach_deps
from ._downloads import get_total_downloads, get_downloads_by_category
from ._editorconfig import make_editorconfig
from ._entrypoints import get_entrypoints
from ._json import make_json
from ._python import get_python, get_python_env
from ._venv import get_venv


__all__ = [
'attach_deps',
'get_downloads_by_category',
'get_entrypoints',
'get_python_env',
'get_python',
'get_total_downloads',
'get_venv',
'make_bash_autocomplete',
'make_editorconfig',
'make_json',
'make_zsh_autocomplete',
]
50 changes: 50 additions & 0 deletions dephell/actions/_autocomplete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from collections import defaultdict

from jinja2 import Environment, PackageLoader


templates = Environment(
loader=PackageLoader('dephell', 'templates'),
)


def make_bash_autocomplete() -> str:
from ..commands import commands

template = templates.get_template('autocomplete.sh.j2')
tree = defaultdict(set)
first_words = set()
for command in commands:
command, _sep, subcommand = command.partition(' ')
first_words.add(command)
if subcommand:
tree[command].add(subcommand)

arguments = defaultdict(set)
for command_name, command in commands.items():
for action in command.get_parser()._actions:
arguments[command_name].update(action.option_strings)

return template.render(first_words=first_words, tree=tree, arguments=arguments)


def make_zsh_autocomplete() -> str:
from ..commands import commands

template = templates.get_template('autocomplete-zsh.sh.j2')
tree = defaultdict(set)
first_words = set()
for command_name, command in commands.items():
command_name, _sep, subcommand = command_name.partition(' ')
first_words.add(command_name)
if subcommand:
description = command.get_parser().description.lstrip().split('\n', maxsplit=1)[0]
tree[command_name].add((subcommand, description))

arguments = defaultdict(list)
for command_name, command in commands.items():
for action in command.get_parser()._actions:
if action.help:
arguments[command_name].append((action.option_strings, action.help))

return template.render(first_words=first_words, tree=tree, arguments=arguments)
34 changes: 34 additions & 0 deletions dephell/actions/_converting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from logging import getLogger

from ..config import Config
from ..converters import CONVERTERS


logger = getLogger('dephell.actions')


def attach_deps(*, resolver, config: Config, merge: bool = True) -> bool:
if 'and' not in config:
return True

# attach
for source in config['and']:
logger.debug('attach dependencies...', extra=dict(
format=source['format'],
path=source['path'],
))
loader = CONVERTERS[source['format']]
root = loader.load(path=source['path'])
resolver.graph.add(root)

if not merge:
return True

# merge (without full graph building)
logger.debug('merging...')
resolved = resolver.resolve(level=1, silent=config['silent'])
if not resolved:
return False
logger.debug('merged')

return True
85 changes: 85 additions & 0 deletions dephell/actions/_downloads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# built-in
from collections import defaultdict
from datetime import date, timedelta
from itertools import zip_longest
from typing import Iterable, Iterator, Dict, List

import attr
import requests


RECENT_URL = 'https://pypistats.org/api/packages/{}/recent'
CATEGORIES_URLS = dict(
pythons='https://pypistats.org/api/packages/{}/python_minor',
systems='https://pypistats.org/api/packages/{}/system',
)


@attr.s()
class DateList:
start = attr.ib()
end = attr.ib()
_data = attr.ib(factory=dict, repr=False)

def add(self, date: str, value: int):
self._data[date] = value

def __iter__(self) -> Iterator[int]:
moment = self.start
while moment <= self.end:
yield self._data.get(str(moment), 0)
moment += timedelta(1)


def make_chart(values: Iterable[int], group: int = None, ticks: str = '_▁▂▃▄▅▆▇█') -> str:
peek = max(values)
if peek == 0:
chart = ticks[-1] * len(values)
else:
chart = ''
for value in values:
index = round((len(ticks) - 1) * value / peek)
chart += ticks[int(index)]
if group:
chunks = map(''.join, zip_longest(*[iter(chart)] * group, fillvalue=' '))
chart = ' '.join(chunks).strip()
return chart


def get_total_downloads(name: str) -> Dict[str, int]:
url = RECENT_URL.format(name)
response = requests.get(url)
response.raise_for_status()
body = response.json()['data']
return dict(
day=body['last_day'],
week=body['last_week'],
month=body['last_month'],
)


def get_downloads_by_category(*, category: str, name: str) -> List[Dict[str, int]]:
url = CATEGORIES_URLS[category].format(name)
response = requests.get(url)
response.raise_for_status()
body = response.json()['data']

yesterday = date.today() - timedelta(1)
grouped = defaultdict(lambda: DateList(start=yesterday - timedelta(30), end=yesterday))
for line in body:
category = line['category'].replace('.', '')
grouped[category].add(date=line['date'], value=line['downloads'])

result = []
for category, downloads in grouped.items():
downloads = list(downloads)
if sum(downloads) == 0:
continue
result.append(dict(
category=category,
day=downloads[-1],
week=sum(downloads[-7:]),
month=sum(downloads),
chart=make_chart(downloads[-28:], group=7),
))
return result
109 changes: 109 additions & 0 deletions dephell/actions/_editorconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# built-in
from pathlib import Path
from typing import Tuple

# external
import attr


@attr.s(frozen=True)
class Rule:
header = attr.ib(type=str)
patterns = attr.ib(type=Tuple[str, ...])
styles = attr.ib(type=Tuple[str, ...])

def match(self, path: Path) -> bool:
for pattern in self.patterns:
iterator = path.glob(pattern)
try:
next(iterator)
except StopIteration:
continue
return True
return False

def __str__(self) -> str:
return '[{header}]\n{styles}'.format(
header=self.header,
styles='\n'.join(self.styles),
)


HEADER = """
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# https://editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
"""

RULES = (
# 4 spaces
Rule(
header='*.py',
patterns=('**/*.py', ),
styles=('indent_style = space', 'indent_size = 4'),
),
Rule(
header='*.{md,rst,txt}',
patterns=('*.md', '*.rst', '*.txt'),
styles=('indent_style = space', 'indent_size = 4'),
),
Rule(
header='*.{ini,toml}',
patterns=('*.ini', '*.toml'),
styles=('indent_style = space', 'indent_size = 4'),
),
Rule(
header='*Dockerfile',
patterns=('*.Dockerfile', 'Dockerfile'),
styles=('indent_style = space', 'indent_size = 4'),
),

# 2 spaces
Rule(
header='*.js',
patterns=('**/*.js', ),
styles=('indent_style = space', 'indent_size = 2'),
),
Rule(
header='*.{json,yml,yaml}',
patterns=('*.json', '*.yml', '*.yaml'),
styles=('indent_style = space', 'indent_size = 2'),
),
Rule(
header='*.{html,html.j2}',
patterns=('**/*.html', '**/*.html.j2'),
styles=('indent_style = space', 'indent_size = 2'),
),

# tabs
Rule(
header='Makefile',
patterns=('Makefile', ),
styles=('indent_style = tab', ),
),
Rule(
header='*.go',
patterns=('*.go', ),
styles=('indent_style = tab', ),
),
)


def make_editorconfig(path: Path) -> str:
matched = []
non_matched = []
for i, rule in enumerate(RULES):
if rule.match(path):
matched.append(rule)
else:
non_matched.append(rule)

return HEADER + '\n\n'.join(map(str, matched)) + '\n'
26 changes: 26 additions & 0 deletions dephell/actions/_entrypoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from logging import getLogger
from typing import Tuple

from ..converters import EggInfoConverter
from ..models import EntryPoint
from ..venvs import VEnv


logger = getLogger('dephell.actions')


def get_entrypoints(*, venv: VEnv, name: str) -> Tuple[EntryPoint, ...]:
if not venv.lib_path:
logger.critical('cannot locate lib path in the venv')
return False
paths = list(venv.lib_path.glob('{}*.*-info'.format(name)))
if not paths:
paths = list(venv.lib_path.glob('{}*.*-info'.format(name.replace('-', '_'))))
if not paths:
logger.critical('cannot locate dist-info for installed package')
return False
path = paths[0] / 'entry_points.txt'
if not path.exists():
logger.error('cannot find any entrypoints for package')
return False
return EggInfoConverter().parse_entrypoints(content=path.read_text()).entrypoints
38 changes: 18 additions & 20 deletions dephell/commands/helpers.py → dephell/actions/_json.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
# built-in
import json
from collections import defaultdict

# external
from dephell_pythons import Python, Pythons

# app
from ..converters import CONVERTERS
from ..config import Config
from functools import reduce
from typing import Optional


def _each(value):
Expand Down Expand Up @@ -73,19 +70,20 @@ def getitem(value, key):
return value[key]


def get_python(config: Config) -> Python:
pythons = Pythons()
def make_json(data, key: str = None, sep: Optional[str] = '-') -> str:
json_params = dict(indent=2, sort_keys=True, ensure_ascii=False)
# print all config
if not key:
return json.dumps(data, **json_params)

# defined in config
python = config.get('python')
if python:
return pythons.get_best(python)
if sep is None:
return json.dumps(data[key], **json_params)

# defined in dependency file
if 'from' in config:
loader = CONVERTERS[config['from']['format']]
root = loader.load(path=config['from']['path'])
if root.python:
return pythons.get_by_spec(root.python)
keys = key.replace('.', sep).split(sep)
value = reduce(getitem, keys, data)
# print config section
if isinstance(value, (dict, list)):
return json.dumps(value, **json_params)

return pythons.current
# print one value
return str(value)

0 comments on commit 6e1d8b4

Please sign in to comment.