Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.intersphinx',
]

Expand Down
29 changes: 29 additions & 0 deletions ets/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

""" Toolkit for environment management and continuous integration.

The :mod:`ets` package provides utilities for environment management
and building continuous integration infrastructure that is used by the
various Enthought Tool Suite projects.

It is designed to be installed in a "bootstrap" environment, such as a
developers' everyday Python environment. Once installed it is designed
provide a toolkit for writing continuous integration and deployment
scripts, and provides a toolkit for doing common operations, such as
creating venv or EDM environments, installing modules, running tests,
linting, and generating documentation.

It could potentially also be used to generate demo environments and run
demo scripts to highlight the capabilities of various libraries.

For an example of usage, see the ``etstool.py`` script in the main ets
directory.
"""
89 changes: 89 additions & 0 deletions ets/click_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

"""
Helpers for creating click-based scripts.
"""

import sys

import click

from .edm import EDM
from .venv import Venv


#: The runtimes that we generally support. Modify this in-place to
#: change what might be supported for a particular script.
supported_runtimes = ["3.5", "3.6", "3.7", "3.8", "3.9"]

#: The default runtime version is the current Python version.
default_runtime = '{}.{}'.format(*sys.version_info[:2])


environment_manager = {
'venv': Venv,
'edm': EDM,
}


def create_environment_manager(ctx, param, value):
""" Create an environment object. """
environment = ctx.params.get('environment')
runtime = ctx.params.get('runtime')
if environment is None:
environment = "ets-test-{runtime}".format(runtime=runtime)
ctx.params['environment'] = environment

cls = environment_manager[value]
return cls(environment, runtime)


runtime_option = click.option(
"--runtime",
default=default_runtime,
type=click.Choice(supported_runtimes),
show_default=True,
help="Python runtime version for the development environment",
)
environment_option = click.option(
"--environment",
default=None,
help="Name of the environment to install",
)
environment_manager_option = click.option(
"--environment-manager",
default='venv',
help="Environment management tool to use",
callback=create_environment_manager
)

editable_option = click.option(
"--editable/--not-editable",
default=False,
help="Install main package in 'editable' mode? [default: --not-editable]",
)
verbose_option = click.option(
"--verbose/--quiet",
default=True,
help="Run tests in verbose mode? [default: --verbose]",
)

docs_option = click.option(
"--docs/--no-docs",
default=True,
help="Install documentation dependencies.",
)

tests_option = click.option(
"--tests/--no-tests",
default=True,
help="Install test dependencies.",
)
224 changes: 224 additions & 0 deletions ets/copyright_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

""" Flake8 plugin to check copyright headers. """

import re

# Minimum end year for the copyright statement.
MINIMUM_END_YEAR = 2020

# Regular expression to match things of the form "2019" or of the form
# "2010-2019".
YEAR_RANGE = r"(?P<start_year>\d{4})(?:\-(?P<end_year>\d{4}))?"

# Generic copyright, used for searching for multiple copyright headers.
GENERIC_COPYRIGHT = re.compile("# .*Copyright .*Enthought", re.IGNORECASE)

# Template for a regular expression for the copyright header.
PRODUCT_CODE_HEADER_TEMPLATE = r"""
# \(C\) Copyright {year_range} {company_name}
# All rights reserved\.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE\.txt and may be redistributed only under
# the conditions described in the aforementioned license\. The license
# is also available online at http://www\.enthought\.com/licenses/BSD\.txt
#
# Thanks for using Enthought open source!
""".lstrip()

ENTHOUGHT_PRODUCT_CODE_HEADER = re.compile(
PRODUCT_CODE_HEADER_TEMPLATE.format(
company_name=r"Enthought, Inc\., Austin, TX", year_range=YEAR_RANGE,
)
)


def parse_years(header_text):
"""
Parse a copyright year range from a header string.

Looks for a year range of the form "2019" or "2010-2019", and
returns the start and end year.

If there are multiple year ranges, parses only the first.

Parameters
----------
header_text : str
The text to be parsed. Could be the entire copyright header,
or a single line from the copyright header.

Returns
-------
start_year, end_year : int
Start year and end year described by the range.
match_pos : int
Position within the string at which the match occurred.

Raises
------
ValueError
If no year range is recognised from the given string.
"""
years_match = re.search(YEAR_RANGE, header_text)
if not years_match:
raise ValueError("No year range found in the given string.")

start_year = int(years_match.group("start_year"))
end_year_str = years_match.group("end_year")
end_year = int(end_year_str) if end_year_str is not None else start_year
return start_year, end_year, years_match.start()


class HeaderError:
"""
Base class for the copyright header errors.
"""

def __init__(self, lineno, col_offset):
self.lineno = lineno
self.col_offset = col_offset

@property
def full_message(self):
"""
Full message in the form expected by flake8 (including the error code).
"""
return "{} {}".format(self.code, self.message)


class MissingCopyrightHeaderError(HeaderError):
"""
Error reported when no copyright header can be identified.
"""

code = "H101"
message = "Missing copyright header"


class DuplicateCopyrightHeaderError(HeaderError):
"""
Error reported if multiple copyright headers found.
"""

code = "H102"
message = "Multiple copyright headers found"


class IncorrectCopyrightHeaderError(HeaderError):
""" Error reported for incorrect copyright header.

Ths error is reported if a copyright header is found, but its wording
doesn't match the officially approved wording.
"""

code = "H103"
message = "Wrong copyright header found"


class OutdatedCopyrightYearError(HeaderError):
""" Error reported for incorrect copyright year information.

This error is reported if the copyright header doesn't have the correct
year information in it.
"""

code = "H104"

def __init__(self, lineno, col_offset, end_year):
super().__init__(lineno=lineno, col_offset=col_offset)
self.end_year = end_year

@property
def message(self):
""" Property returning the error message. """
return (
"Copyright end year ({}) out of date. The year should be at "
"least {}.".format(self.end_year, MINIMUM_END_YEAR)
)


def copyright_header(lines):
"""
Check copyright header presence and accuracy in a Python file.
"""
file_contents = "".join(lines)

# Empty files don't need a copyright header.
if not file_contents:
return

# Not an empty file. See if we have a copyright header at all.
copyrights_found = []
for lineno, line in enumerate(lines, start=1):
if re.match(GENERIC_COPYRIGHT, line):
copyrights_found.append(lineno)

if not copyrights_found:
yield MissingCopyrightHeaderError(
lineno=1, col_offset=0,
)
return

if len(copyrights_found) > 1:
# Multiple possible copyright statements; report each one
# beyond the first, but we still check the first for
# correctness below.
for lineno in copyrights_found[1:]:
yield DuplicateCopyrightHeaderError(
lineno=lineno, col_offset=0,
)

# Check that the first copyright statement is the right one.
remaining_file_content = "".join(lines[copyrights_found[0]-1:])
header_match = ENTHOUGHT_PRODUCT_CODE_HEADER.match(remaining_file_content)
if header_match is None:
yield IncorrectCopyrightHeaderError(
lineno=1, col_offset=0,
)
return

# Check the year range in the header.
for lineno, line in enumerate(lines, start=1):
try:
start_year, end_year, match_pos = parse_years(line)
except ValueError:
pass
else:
break

if end_year < MINIMUM_END_YEAR:
yield OutdatedCopyrightYearError(
lineno=lineno, col_offset=match_pos, end_year=end_year,
)


class CopyrightHeaderExtension(object):
"""
Flake8 extension for checking ETS copyright headers.
"""

name = "headers"
version = "1.1.0"

def __init__(self, tree, lines):
self.lines = lines

def run(self):
""" Run the copyright header plugin. """
for error in copyright_header(self.lines):
yield (
error.lineno,
error.col_offset,
error.full_message,
type(self),
)
Loading