diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 0a0e9a16..df920e4e 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -14,15 +14,19 @@ jobs: python-version: "3.6" - os: windows-latest python-version: "3.10" + runs-on: ${{ matrix.os }} + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: "recursive" + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip @@ -31,11 +35,8 @@ jobs: - name: Python Lint run: | tox -e lint + - name: Integration Tests - env: - TEST_EMAIL_LOGIN: ${{ secrets.TEST_EMAIL_LOGIN }} - TEST_EMAIL_PASSWORD: ${{ secrets.TEST_EMAIL_PASSWORD }} - TEST_EMAIL_IMAP_SERVER: ${{ secrets.TEST_EMAIL_IMAP_SERVER }} run: | tox -e testcore diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 00000000..d961da8c --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,42 @@ +name: Deployment + +on: + push: + branches: + - master + - "release/**" + +jobs: + deployment: + runs-on: ubuntu-latest + environment: production + + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.9" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Deployment Tests + env: + TEST_EMAIL_LOGIN: ${{ secrets.TEST_EMAIL_LOGIN }} + TEST_EMAIL_PASSWORD: ${{ secrets.TEST_EMAIL_PASSWORD }} + TEST_EMAIL_IMAP_SERVER: ${{ secrets.TEST_EMAIL_IMAP_SERVER }} + run: | + tox -e testcore + + - name: Publish package to PyPI + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 6e9fc7df..115817c4 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -2,22 +2,28 @@ name: Examples on: [push, pull_request] + jobs: build: strategy: fail-fast: false matrix: - os: [ubuntu-18.04, windows-latest, macos-latest] - python-version: [3.7] + os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} + env: + PIO_INSTALL_DEVPLATFORM_OWNERNAMES: "platformio" + PIO_INSTALL_DEVPLATFORM_NAMES: "aceinna_imu,atmelavr,atmelmegaavr,atmelsam,espressif32,espressif8266,nordicnrf52,raspberrypi,ststm32,teensy" + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: "recursive" - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + + - name: Set up Python + uses: actions/setup-python@v3 with: - python-version: ${{ matrix.python-version }} + python-version: "3.9" + - name: Install dependencies run: | python -m pip install --upgrade pip @@ -25,24 +31,15 @@ jobs: - name: Run on Linux if: startsWith(matrix.os, 'ubuntu') - env: - PIO_INSTALL_DEVPLATFORMS_OWNERNAMES: "platformio" - PIO_INSTALL_DEVPLATFORMS_IGNORE: "ststm8,infineonxmc,intel_mcs51" run: | - # ChipKIT issue: install 32-bit support for GCC PIC32 - sudo apt-get install libc6-i386 # Free space sudo apt clean docker rmi $(docker image ls -aq) df -h - # Run tox -e testexamples - name: Run on macOS if: startsWith(matrix.os, 'macos') - env: - PIO_INSTALL_DEVPLATFORMS_OWNERNAMES: "platformio" - PIO_INSTALL_DEVPLATFORMS_IGNORE: "ststm8,infineonxmc,microchippic32,lattice_ice40,gd32v" run: | df -h tox -e testexamples @@ -52,8 +49,6 @@ jobs: env: PLATFORMIO_CORE_DIR: C:/pio PLATFORMIO_WORKSPACE_DIR: C:/pio-workspace/$PROJECT_HASH - PIO_INSTALL_DEVPLATFORMS_OWNERNAMES: "platformio" - PIO_INSTALL_DEVPLATFORMS_IGNORE: "ststm8,infineonxmc,riscv_gap" run: | tox -e testexamples diff --git a/.github/workflows/projects.yml b/.github/workflows/projects.yml new file mode 100644 index 00000000..1e21bed1 --- /dev/null +++ b/.github/workflows/projects.yml @@ -0,0 +1,69 @@ +name: Projects + +on: [push, pull_request] + +jobs: + build: + strategy: + fail-fast: false + matrix: + project: + - marlin: + repository: "MarlinFirmware/Marlin" + folder: "Marlin" + config_dir: "Marlin" + env_name: "mega2560" + - esphome: + repository: "esphome/esphome" + folder: "esphome" + config_dir: "esphome" + env_name: "esp32-arduino" + - smartknob: + repository: "scottbez1/smartknob" + folder: "smartknob" + config_dir: "smartknob/firmware" + env_name: "view" + - espurna: + repository: "xoseperez/espurna" + folder: "espurna" + config_dir: "espurna/code" + env_name: "nodemcu-lolin" + - OpenMQTTGateway: + repository: "1technophile/OpenMQTTGateway" + folder: "OpenMQTTGateway" + config_dir: "OpenMQTTGateway" + env_name: "esp32-m5atom" + os: [ubuntu-latest, windows-latest, macos-latest] + exclude: + - os: windows-latest + project: {"esphome": "", "repository": "esphome/esphome", "folder": "esphome", "config_dir": "esphome", "env_name": "esp32-arduino"} + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: 3.9 + + - name: Install PlatformIO + run: pip install -U . + + - name: Check out ${{ matrix.project.repository }} + uses: actions/checkout@v2 + with: + submodules: "recursive" + repository: ${{ matrix.project.repository }} + path: ${{ matrix.project.folder }} + + - name: Install ESPHome dependencies + # Requires esptool package as it's used in a custom prescript + if: ${{ contains(matrix.project.repository, 'esphome') }} + run: pip install esptool==3.* + + - name: Compile ${{ matrix.project.repository }} + run: pio run -d ${{ matrix.project.config_dir }} -e ${{ matrix.project.env_name }} + diff --git a/HISTORY.rst b/HISTORY.rst index 9426f0dd..e37671e5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,7 @@ Release Notes .. |PIOCONF| replace:: `"platformio.ini" `__ configuration file .. |LDF| replace:: `LDF `__ .. |INTERPOLATION| replace:: `Interpolation of Values `__ +.. |UNITTESTING| replace:: `Unit Testing `__ .. _release_notes_6: @@ -12,6 +13,16 @@ PlatformIO Core 6 **A professional collaborative platform for declarative, safety-critical, and test-driven embedded development.** +6.0.2 (2022-06-01) +~~~~~~~~~~~~~~~~~~ + +* Control |UNITTESTING| verbosity with a new multilevel `pio test -v `__ command option (`issue #4276 `_) +* Follow symbolic links during searching for the unit test suites (`issue #4288 `_) +* Show a warning when testing an empty project without a test suite (`issue #4278 `_) +* Improved support for `Asking for input (prompts) `_ +* Fixed an issue when the `build_src_flags `__ option was applied outside the project scope (`issue #4277 `_) +* Fixed an issue with debugging assembly files without preprocessor (".s") + 6.0.1 (2022-05-17) ~~~~~~~~~~~~~~~~~~ @@ -53,7 +64,7 @@ Please check the `Migration guide from 5.x to 6.0 `_ solution and its documentation + - Refactored from scratch |UNITTESTING| solution and its documentation - New: `Test Hierarchy `_ (`issue #4135 `_) - New: `Doctest `__ testing framework (`issue #4240 `_) - New: `GoogleTest `__ testing and mocking framework (`issue #3572 `_) diff --git a/README.rst b/README.rst index ff10f8f2..4bdda76f 100644 --- a/README.rst +++ b/README.rst @@ -8,18 +8,18 @@ PlatformIO Core .. image:: https://github.com/platformio/platformio-core/workflows/Core/badge.svg :target: https://docs.platformio.org/en/latest/core/index.html :alt: CI Build for PlatformIO Core -.. image:: https://github.com/platformio/platformio-core/workflows/Examples/badge.svg - :target: https://github.com/platformio/platformio-examples - :alt: CI Build for dev-platform examples .. image:: https://github.com/platformio/platformio-core/workflows/Docs/badge.svg :target: https://docs.platformio.org?utm_source=github&utm_medium=core :alt: CI Build for Docs +.. image:: https://github.com/platformio/platformio-core/workflows/Examples/badge.svg + :target: https://github.com/platformio/platformio-examples + :alt: CI Build for dev-platform examples +.. image:: https://github.com/platformio/platformio-core/workflows/Projects/badge.svg + :target: https://docs.platformio.org/en/latest/tutorials/index.html#projects + :alt: CI Build for the Community Projects .. image:: https://img.shields.io/pypi/v/platformio.svg :target: https://pypi.python.org/pypi/platformio/ :alt: Latest Version -.. image:: https://img.shields.io/badge/license-Apache%202.0-blue.svg - :target: https://pypi.python.org/pypi/platformio/ - :alt: License .. image:: https://img.shields.io/badge/PlatformIO-Labs-orange.svg :alt: PlatformIO Labs :target: https://piolabs.com/?utm_source=github&utm_medium=core diff --git a/docs b/docs index 5bf0037c..300060ea 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 5bf0037c6679cccffbb835c30d729cd19120651b +Subproject commit 300060ea08be494465b03b427186bee66eda1766 diff --git a/examples b/examples index 8464bbb5..6c52fd32 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 8464bbb5d96022ade33f92ca829c6401fb067d6a +Subproject commit 6c52fd327753f2ca14b575bd8719674b479e1181 diff --git a/platformio/__init__.py b/platformio/__init__.py index 097088c0..7fe4bec7 100644 --- a/platformio/__init__.py +++ b/platformio/__init__.py @@ -14,7 +14,7 @@ import sys -VERSION = (6, 0, 1) +VERSION = (6, 0, 2) __version__ = ".".join([str(s) for s in VERSION]) __title__ = "platformio" diff --git a/platformio/__main__.py b/platformio/__main__.py index 07816eda..c7f8e245 100644 --- a/platformio/__main__.py +++ b/platformio/__main__.py @@ -19,7 +19,7 @@ from traceback import format_exc import click from platformio import __version__, exception, maintenance -from platformio.commands import PlatformioCLI +from platformio.cli import PlatformioCLI from platformio.compat import IS_CYGWIN, ensure_python3 @@ -27,7 +27,7 @@ from platformio.compat import IS_CYGWIN, ensure_python3 cls=PlatformioCLI, context_settings=dict(help_option_names=["-h", "--help"]) ) @click.version_option(__version__, prog_name="PlatformIO Core") -@click.option("--force", "-f", is_flag=True, help="DEPRECATED") +@click.option("--force", "-f", is_flag=True, help="DEPRECATED", hidden=True) @click.option("--caller", "-c", help="Caller ID (service)") @click.option("--no-ansi", is_flag=True, help="Do not print ANSI control characters") @click.pass_context @@ -120,7 +120,7 @@ An unexpected error occurred. Further steps: `pip install -U platformio` command * Try to find answer in FAQ Troubleshooting section - https://docs.platformio.org/page/faq.html + https://docs.platformio.org/page/faq/index.html * Report this problem to the developers https://github.com/platformio/platformio-core/issues diff --git a/platformio/clients/__init__.py b/platformio/account/__init__.py similarity index 100% rename from platformio/clients/__init__.py rename to platformio/account/__init__.py diff --git a/platformio/account/cli.py b/platformio/account/cli.py new file mode 100644 index 00000000..135445d4 --- /dev/null +++ b/platformio/account/cli.py @@ -0,0 +1,44 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.commands.destroy import account_destroy_cmd +from platformio.account.commands.forgot import account_forgot_cmd +from platformio.account.commands.login import account_login_cmd +from platformio.account.commands.logout import account_logout_cmd +from platformio.account.commands.password import account_password_cmd +from platformio.account.commands.register import account_register_cmd +from platformio.account.commands.show import account_show_cmd +from platformio.account.commands.token import account_token_cmd +from platformio.account.commands.update import account_update_cmd + + +@click.group( + "account", + commands=[ + account_destroy_cmd, + account_forgot_cmd, + account_login_cmd, + account_logout_cmd, + account_password_cmd, + account_register_cmd, + account_show_cmd, + account_token_cmd, + account_update_cmd, + ], + short_help="Manage PlatformIO account", +) +def cli(): + pass diff --git a/platformio/clients/account.py b/platformio/account/client.py similarity index 99% rename from platformio/clients/account.py rename to platformio/account/client.py index 2afe6fbe..a2eb0c28 100644 --- a/platformio/clients/account.py +++ b/platformio/account/client.py @@ -16,8 +16,8 @@ import os import time from platformio import __accounts_api__, app -from platformio.clients.http import HTTPClient, HTTPClientError from platformio.exception import PlatformioException +from platformio.http import HTTPClient, HTTPClientError class AccountError(PlatformioException): diff --git a/platformio/commands/check/__init__.py b/platformio/account/commands/__init__.py similarity index 100% rename from platformio/commands/check/__init__.py rename to platformio/account/commands/__init__.py diff --git a/platformio/account/commands/destroy.py b/platformio/account/commands/destroy.py new file mode 100644 index 00000000..5a4f4dd6 --- /dev/null +++ b/platformio/account/commands/destroy.py @@ -0,0 +1,37 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient, AccountNotAuthorized + + +@click.command("destroy", short_help="Destroy account") +def account_destroy_cmd(): + client = AccountClient() + click.confirm( + "Are you sure you want to delete the %s user account?\n" + "Warning! All linked data will be permanently removed and can not be restored." + % client.get_logged_username(), + abort=True, + ) + client.destroy_account() + try: + client.logout() + except AccountNotAuthorized: + pass + click.secho( + "User account has been destroyed.", + fg="green", + ) diff --git a/tests/commands/test_update.py b/platformio/account/commands/forgot.py similarity index 57% rename from tests/commands/test_update.py rename to platformio/account/commands/forgot.py index 6edddfa3..4af8f5fa 100644 --- a/tests/commands/test_update.py +++ b/platformio/account/commands/forgot.py @@ -12,16 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=unused-argument +import click -from platformio.commands.update import cli as cmd_update +from platformio.account.client import AccountClient -def test_update(clirunner, validate_cliresult, isolated_pio_core): - matches = ("Platform Manager", "Library Manager") - result = clirunner.invoke(cmd_update) - validate_cliresult(result) - assert all(m in result.output for m in matches) - result = clirunner.invoke(cmd_update) - validate_cliresult(result) - assert all(m in result.output for m in matches) +@click.command("forgot", short_help="Forgot password") +@click.option("--username", prompt="Username or email") +def account_forgot_cmd(username): + client = AccountClient() + client.forgot_password(username) + click.secho( + "If this account is registered, we will send the " + "further instructions to your email.", + fg="green", + ) diff --git a/platformio/account/commands/login.py b/platformio/account/commands/login.py new file mode 100644 index 00000000..b9471bfd --- /dev/null +++ b/platformio/account/commands/login.py @@ -0,0 +1,26 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient + + +@click.command("login", short_help="Log in to PlatformIO Account") +@click.option("-u", "--username", prompt="Username or email") +@click.option("-p", "--password", prompt=True, hide_input=True) +def account_login_cmd(username, password): + client = AccountClient() + client.login(username, password) + click.secho("Successfully logged in!", fg="green") diff --git a/platformio/account/commands/logout.py b/platformio/account/commands/logout.py new file mode 100644 index 00000000..243a7f14 --- /dev/null +++ b/platformio/account/commands/logout.py @@ -0,0 +1,24 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient + + +@click.command("logout", short_help="Log out of PlatformIO Account") +def account_logout_cmd(): + client = AccountClient() + client.logout() + click.secho("Successfully logged out!", fg="green") diff --git a/platformio/account/commands/password.py b/platformio/account/commands/password.py new file mode 100644 index 00000000..4f129c08 --- /dev/null +++ b/platformio/account/commands/password.py @@ -0,0 +1,26 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient + + +@click.command("password", short_help="Change password") +@click.option("--old-password", prompt=True, hide_input=True) +@click.option("--new-password", prompt=True, hide_input=True, confirmation_prompt=True) +def account_password_cmd(old_password, new_password): + client = AccountClient() + client.change_password(old_password, new_password) + click.secho("Password successfully changed!", fg="green") diff --git a/platformio/account/commands/register.py b/platformio/account/commands/register.py new file mode 100644 index 00000000..6fc20ce6 --- /dev/null +++ b/platformio/account/commands/register.py @@ -0,0 +1,52 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient +from platformio.account.validate import ( + validate_email, + validate_password, + validate_username, +) + + +@click.command("register", short_help="Create new PlatformIO Account") +@click.option( + "-u", + "--username", + prompt=True, + callback=lambda _, __, value: validate_username(value), +) +@click.option( + "-e", "--email", prompt=True, callback=lambda _, __, value: validate_email(value) +) +@click.option( + "-p", + "--password", + prompt=True, + hide_input=True, + confirmation_prompt=True, + callback=lambda _, __, value: validate_password(value), +) +@click.option("--firstname", prompt=True) +@click.option("--lastname", prompt=True) +def account_register_cmd(username, email, password, firstname, lastname): + client = AccountClient() + client.registration(username, email, password, firstname, lastname) + click.secho( + "An account has been successfully created. " + "Please check your mail to activate your account and verify your email address.", + fg="green", + ) diff --git a/platformio/account/commands/show.py b/platformio/account/commands/show.py new file mode 100644 index 00000000..f107f4f5 --- /dev/null +++ b/platformio/account/commands/show.py @@ -0,0 +1,116 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import click +from tabulate import tabulate + +from platformio import util +from platformio.account.client import AccountClient + + +@click.command("show", short_help="PlatformIO Account information") +@click.option("--offline", is_flag=True) +@click.option("--json-output", is_flag=True) +def account_show_cmd(offline, json_output): + client = AccountClient() + info = client.get_account_info(offline) + if json_output: + click.echo(json.dumps(info)) + return + click.echo() + if info.get("profile"): + print_profile(info["profile"]) + if info.get("packages"): + print_packages(info["packages"]) + if info.get("subscriptions"): + print_subscriptions(info["subscriptions"]) + click.echo() + + +def print_profile(profile): + click.secho("Profile", fg="cyan", bold=True) + click.echo("=" * len("Profile")) + data = [] + if profile.get("username"): + data.append(("Username:", profile["username"])) + if profile.get("email"): + data.append(("Email:", profile["email"])) + if profile.get("firstname"): + data.append(("First name:", profile["firstname"])) + if profile.get("lastname"): + data.append(("Last name:", profile["lastname"])) + click.echo(tabulate(data, tablefmt="plain")) + + +def print_packages(packages): + click.echo() + click.secho("Packages", fg="cyan") + click.echo("=" * len("Packages")) + for package in packages: + click.echo() + click.secho(package.get("name"), bold=True) + click.echo("-" * len(package.get("name"))) + if package.get("description"): + click.echo(package.get("description")) + data = [] + expire = "-" + if "subscription" in package: + expire = util.parse_datetime( + package["subscription"].get("end_at") + or package["subscription"].get("next_bill_at") + ).strftime("%Y-%m-%d") + data.append(("Expire:", expire)) + services = [] + for key in package: + if not key.startswith("service."): + continue + if isinstance(package[key], dict): + services.append(package[key].get("title")) + else: + services.append(package[key]) + if services: + data.append(("Services:", ", ".join(services))) + click.echo(tabulate(data, tablefmt="plain")) + + +def print_subscriptions(subscriptions): + click.echo() + click.secho("Subscriptions", fg="cyan") + click.echo("=" * len("Subscriptions")) + for subscription in subscriptions: + click.echo() + click.secho(subscription.get("product_name"), bold=True) + click.echo("-" * len(subscription.get("product_name"))) + data = [("State:", subscription.get("status"))] + begin_at = util.parse_datetime(subscription.get("begin_at")).strftime("%c") + data.append(("Start date:", begin_at or "-")) + end_at = subscription.get("end_at") + if end_at: + end_at = util.parse_datetime(subscription.get("end_at")).strftime("%c") + data.append(("End date:", end_at or "-")) + next_bill_at = subscription.get("next_bill_at") + if next_bill_at: + next_bill_at = util.parse_datetime( + subscription.get("next_bill_at") + ).strftime("%c") + data.append(("Next payment:", next_bill_at or "-")) + data.append( + ("Edit:", click.style(subscription.get("update_url"), fg="blue") or "-") + ) + data.append( + ("Cancel:", click.style(subscription.get("cancel_url"), fg="blue") or "-") + ) + click.echo(tabulate(data, tablefmt="plain")) diff --git a/platformio/account/commands/token.py b/platformio/account/commands/token.py new file mode 100644 index 00000000..0f06e367 --- /dev/null +++ b/platformio/account/commands/token.py @@ -0,0 +1,32 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import click + +from platformio.account.client import AccountClient + + +@click.command("token", short_help="Get or regenerate Authentication Token") +@click.option("-p", "--password", prompt=True, hide_input=True) +@click.option("--regenerate", is_flag=True) +@click.option("--json-output", is_flag=True) +def account_token_cmd(password, regenerate, json_output): + client = AccountClient() + auth_token = client.auth_token(password, regenerate) + if json_output: + click.echo(json.dumps({"status": "success", "result": auth_token})) + return + click.secho("Personal Authentication Token: %s" % auth_token, fg="green") diff --git a/platformio/account/commands/update.py b/platformio/account/commands/update.py new file mode 100644 index 00000000..b3868938 --- /dev/null +++ b/platformio/account/commands/update.py @@ -0,0 +1,59 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient, AccountNotAuthorized +from platformio.account.validate import validate_email, validate_username + + +@click.command("update", short_help="Update profile information") +@click.option("--current-password", prompt=True, hide_input=True) +@click.option("--username") +@click.option("--email") +@click.option("--firstname") +@click.option("--lastname") +def account_update_cmd(current_password, **kwargs): + client = AccountClient() + profile = client.get_profile() + new_profile = profile.copy() + if not any(kwargs.values()): + for field in profile: + new_profile[field] = click.prompt( + field.replace("_", " ").capitalize(), default=profile[field] + ) + if field == "email": + validate_email(new_profile[field]) + if field == "username": + validate_username(new_profile[field]) + else: + new_profile.update({key: value for key, value in kwargs.items() if value}) + client.update_profile(new_profile, current_password) + click.secho("Profile successfully updated!", fg="green") + username_changed = new_profile["username"] != profile["username"] + email_changed = new_profile["email"] != profile["email"] + if not username_changed and not email_changed: + return None + try: + client.logout() + except AccountNotAuthorized: + pass + if email_changed: + click.secho( + "Please check your mail to verify your new email address and re-login. ", + fg="yellow", + ) + return None + click.secho("Please re-login.", fg="yellow") + return None diff --git a/platformio/commands/home/__init__.py b/platformio/account/org/__init__.py similarity index 100% rename from platformio/commands/home/__init__.py rename to platformio/account/org/__init__.py diff --git a/platformio/account/org/cli.py b/platformio/account/org/cli.py new file mode 100644 index 00000000..5f1f0fc9 --- /dev/null +++ b/platformio/account/org/cli.py @@ -0,0 +1,38 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.org.commands.add import org_add_cmd +from platformio.account.org.commands.create import org_create_cmd +from platformio.account.org.commands.destroy import org_destroy_cmd +from platformio.account.org.commands.list import org_list_cmd +from platformio.account.org.commands.remove import org_remove_cmd +from platformio.account.org.commands.update import org_update_cmd + + +@click.group( + "account", + commands=[ + org_add_cmd, + org_create_cmd, + org_destroy_cmd, + org_list_cmd, + org_remove_cmd, + org_update_cmd, + ], + short_help="Manage organizations", +) +def cli(): + pass diff --git a/platformio/commands/home/rpc/__init__.py b/platformio/account/org/commands/__init__.py similarity index 100% rename from platformio/commands/home/rpc/__init__.py rename to platformio/account/org/commands/__init__.py diff --git a/platformio/account/org/commands/add.py b/platformio/account/org/commands/add.py new file mode 100644 index 00000000..55e658e7 --- /dev/null +++ b/platformio/account/org/commands/add.py @@ -0,0 +1,34 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient + + +@click.command("add", short_help="Add a new owner to organization") +@click.argument( + "orgname", +) +@click.argument( + "username", +) +def org_add_cmd(orgname, username): + client = AccountClient() + client.add_org_owner(orgname, username) + return click.secho( + "The new owner `%s` has been successfully added to the `%s` organization." + % (username, orgname), + fg="green", + ) diff --git a/platformio/account/org/commands/create.py b/platformio/account/org/commands/create.py new file mode 100644 index 00000000..b0c86bf5 --- /dev/null +++ b/platformio/account/org/commands/create.py @@ -0,0 +1,38 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient +from platformio.account.validate import validate_email, validate_orgname + + +@click.command("create", short_help="Create a new organization") +@click.argument( + "orgname", + callback=lambda _, __, value: validate_orgname(value), +) +@click.option( + "--email", callback=lambda _, __, value: validate_email(value) if value else value +) +@click.option( + "--displayname", +) +def org_create_cmd(orgname, email, displayname): + client = AccountClient() + client.create_org(orgname, email, displayname) + return click.secho( + "The organization `%s` has been successfully created." % orgname, + fg="green", + ) diff --git a/platformio/account/org/commands/destroy.py b/platformio/account/org/commands/destroy.py new file mode 100644 index 00000000..2a202f9a --- /dev/null +++ b/platformio/account/org/commands/destroy.py @@ -0,0 +1,34 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient + + +@click.command("destroy", short_help="Destroy organization") +@click.argument("orgname") +def org_destroy_cmd(orgname): + client = AccountClient() + click.confirm( + "Are you sure you want to delete the `%s` organization account?\n" + "Warning! All linked data will be permanently removed and can not be restored." + % orgname, + abort=True, + ) + client.destroy_org(orgname) + return click.secho( + "Organization `%s` has been destroyed." % orgname, + fg="green", + ) diff --git a/platformio/account/org/commands/list.py b/platformio/account/org/commands/list.py new file mode 100644 index 00000000..471814ea --- /dev/null +++ b/platformio/account/org/commands/list.py @@ -0,0 +1,48 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import click +from tabulate import tabulate + +from platformio.account.client import AccountClient + + +@click.command("list", short_help="List organizations and their members") +@click.option("--json-output", is_flag=True) +def org_list_cmd(json_output): + client = AccountClient() + orgs = client.list_orgs() + if json_output: + return click.echo(json.dumps(orgs)) + if not orgs: + return click.echo("You do not have any organization") + for org in orgs: + click.echo() + click.secho(org.get("orgname"), fg="cyan") + click.echo("-" * len(org.get("orgname"))) + data = [] + if org.get("displayname"): + data.append(("Display Name:", org.get("displayname"))) + if org.get("email"): + data.append(("Email:", org.get("email"))) + data.append( + ( + "Owners:", + ", ".join((owner.get("username") for owner in org.get("owners"))), + ) + ) + click.echo(tabulate(data, tablefmt="plain")) + return click.echo() diff --git a/platformio/account/org/commands/remove.py b/platformio/account/org/commands/remove.py new file mode 100644 index 00000000..9dee462a --- /dev/null +++ b/platformio/account/org/commands/remove.py @@ -0,0 +1,34 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient + + +@click.command("remove", short_help="Remove an owner from organization") +@click.argument( + "orgname", +) +@click.argument( + "username", +) +def org_remove_cmd(orgname, username): + client = AccountClient() + client.remove_org_owner(orgname, username) + return click.secho( + "The `%s` owner has been successfully removed from the `%s` organization." + % (username, orgname), + fg="green", + ) diff --git a/platformio/account/org/commands/update.py b/platformio/account/org/commands/update.py new file mode 100644 index 00000000..9da9564c --- /dev/null +++ b/platformio/account/org/commands/update.py @@ -0,0 +1,52 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient +from platformio.account.validate import validate_email, validate_orgname + + +@click.command("update", short_help="Update organization") +@click.argument("cur_orgname") +@click.option( + "--orgname", + callback=lambda _, __, value: validate_orgname(value), + help="A new orgname", +) +@click.option("--email") +@click.option("--displayname") +def org_update_cmd(cur_orgname, **kwargs): + client = AccountClient() + org = client.get_org(cur_orgname) + del org["owners"] + new_org = org.copy() + if not any(kwargs.values()): + for field in org: + new_org[field] = click.prompt( + field.replace("_", " ").capitalize(), default=org[field] + ) + if field == "email": + validate_email(new_org[field]) + if field == "orgname": + validate_orgname(new_org[field]) + else: + new_org.update( + {key.replace("new_", ""): value for key, value in kwargs.items() if value} + ) + client.update_org(cur_orgname, new_org) + return click.secho( + "The organization `%s` has been successfully updated." % cur_orgname, + fg="green", + ) diff --git a/platformio/commands/home/rpc/handlers/__init__.py b/platformio/account/team/__init__.py similarity index 100% rename from platformio/commands/home/rpc/handlers/__init__.py rename to platformio/account/team/__init__.py diff --git a/platformio/account/team/cli.py b/platformio/account/team/cli.py new file mode 100644 index 00000000..feffa7b3 --- /dev/null +++ b/platformio/account/team/cli.py @@ -0,0 +1,38 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.team.commands.add import team_add_cmd +from platformio.account.team.commands.create import team_create_cmd +from platformio.account.team.commands.destroy import team_destroy_cmd +from platformio.account.team.commands.list import team_list_cmd +from platformio.account.team.commands.remove import team_remove_cmd +from platformio.account.team.commands.update import team_update_cmd + + +@click.group( + "team", + commands=[ + team_add_cmd, + team_create_cmd, + team_destroy_cmd, + team_list_cmd, + team_remove_cmd, + team_update_cmd, + ], + short_help="Manage organization teams", +) +def cli(): + pass diff --git a/platformio/commands/remote/__init__.py b/platformio/account/team/commands/__init__.py similarity index 100% rename from platformio/commands/remote/__init__.py rename to platformio/account/team/commands/__init__.py diff --git a/platformio/account/team/commands/add.py b/platformio/account/team/commands/add.py new file mode 100644 index 00000000..854675e6 --- /dev/null +++ b/platformio/account/team/commands/add.py @@ -0,0 +1,38 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient +from platformio.account.validate import validate_orgname_teamname + + +@click.command("add", short_help="Add a new member to team") +@click.argument( + "orgname_teamname", + metavar="ORGNAME:TEAMNAME", + callback=lambda _, __, value: validate_orgname_teamname(value), +) +@click.argument( + "username", +) +def team_add_cmd(orgname_teamname, username): + orgname, teamname = orgname_teamname.split(":", 1) + client = AccountClient() + client.add_team_member(orgname, teamname, username) + return click.secho( + "The new member %s has been successfully added to the %s team." + % (username, teamname), + fg="green", + ) diff --git a/platformio/account/team/commands/create.py b/platformio/account/team/commands/create.py new file mode 100644 index 00000000..891d33b4 --- /dev/null +++ b/platformio/account/team/commands/create.py @@ -0,0 +1,39 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient +from platformio.account.validate import validate_orgname_teamname + + +@click.command("create", short_help="Create a new team") +@click.argument( + "orgname_teamname", + metavar="ORGNAME:TEAMNAME", + callback=lambda _, __, value: validate_orgname_teamname( + value, teamname_validate=True + ), +) +@click.option( + "--description", +) +def team_create_cmd(orgname_teamname, description): + orgname, teamname = orgname_teamname.split(":", 1) + client = AccountClient() + client.create_team(orgname, teamname, description) + return click.secho( + "The team %s has been successfully created." % teamname, + fg="green", + ) diff --git a/platformio/account/team/commands/destroy.py b/platformio/account/team/commands/destroy.py new file mode 100644 index 00000000..7cd084a1 --- /dev/null +++ b/platformio/account/team/commands/destroy.py @@ -0,0 +1,40 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient +from platformio.account.validate import validate_orgname_teamname + + +@click.command("destroy", short_help="Destroy a team") +@click.argument( + "orgname_teamname", + metavar="ORGNAME:TEAMNAME", + callback=lambda _, __, value: validate_orgname_teamname(value), +) +def team_destroy_cmd(orgname_teamname): + orgname, teamname = orgname_teamname.split(":", 1) + click.confirm( + click.style( + "Are you sure you want to destroy the %s team?" % teamname, fg="yellow" + ), + abort=True, + ) + client = AccountClient() + client.destroy_team(orgname, teamname) + return click.secho( + "The team %s has been successfully destroyed." % teamname, + fg="green", + ) diff --git a/platformio/account/team/commands/list.py b/platformio/account/team/commands/list.py new file mode 100644 index 00000000..e41e872d --- /dev/null +++ b/platformio/account/team/commands/list.py @@ -0,0 +1,59 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import click +from tabulate import tabulate + +from platformio.account.client import AccountClient + + +@click.command("list", short_help="List teams") +@click.argument("orgname", required=False) +@click.option("--json-output", is_flag=True) +def team_list_cmd(orgname, json_output): + client = AccountClient() + data = {} + if not orgname: + for item in client.list_orgs(): + teams = client.list_teams(item.get("orgname")) + data[item.get("orgname")] = teams + else: + teams = client.list_teams(orgname) + data[orgname] = teams + if json_output: + return click.echo(json.dumps(data[orgname] if orgname else data)) + if not any(data.values()): + return click.secho("You do not have any teams.", fg="yellow") + for org_name in data: + for team in data[org_name]: + click.echo() + click.secho("%s:%s" % (org_name, team.get("name")), fg="cyan") + click.echo("-" * len("%s:%s" % (org_name, team.get("name")))) + table_data = [] + if team.get("description"): + table_data.append(("Description:", team.get("description"))) + table_data.append( + ( + "Members:", + ", ".join( + (member.get("username") for member in team.get("members")) + ) + if team.get("members") + else "-", + ) + ) + click.echo(tabulate(table_data, tablefmt="plain")) + return click.echo() diff --git a/platformio/account/team/commands/remove.py b/platformio/account/team/commands/remove.py new file mode 100644 index 00000000..73c79602 --- /dev/null +++ b/platformio/account/team/commands/remove.py @@ -0,0 +1,36 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient +from platformio.account.validate import validate_orgname_teamname + + +@click.command("remove", short_help="Remove a member from team") +@click.argument( + "orgname_teamname", + metavar="ORGNAME:TEAMNAME", + callback=lambda _, __, value: validate_orgname_teamname(value), +) +@click.argument("username") +def team_remove_cmd(orgname_teamname, username): + orgname, teamname = orgname_teamname.split(":", 1) + client = AccountClient() + client.remove_team_member(orgname, teamname, username) + return click.secho( + "The %s member has been successfully removed from the %s team." + % (username, teamname), + fg="green", + ) diff --git a/platformio/account/team/commands/update.py b/platformio/account/team/commands/update.py new file mode 100644 index 00000000..3ead0fed --- /dev/null +++ b/platformio/account/team/commands/update.py @@ -0,0 +1,55 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.account.client import AccountClient +from platformio.account.validate import validate_orgname_teamname, validate_teamname + + +@click.command("update", short_help="Update team") +@click.argument( + "orgname_teamname", + metavar="ORGNAME:TEAMNAME", + callback=lambda _, __, value: validate_orgname_teamname(value), +) +@click.option( + "--name", + callback=lambda _, __, value: validate_teamname(value), + help="A new team name", +) +@click.option( + "--description", +) +def team_update_cmd(orgname_teamname, **kwargs): + orgname, teamname = orgname_teamname.split(":", 1) + client = AccountClient() + team = client.get_team(orgname, teamname) + del team["id"] + del team["members"] + new_team = team.copy() + if not any(kwargs.values()): + for field in team: + new_team[field] = click.prompt( + field.replace("_", " ").capitalize(), default=team[field] + ) + if field == "name": + validate_teamname(new_team[field]) + else: + new_team.update({key: value for key, value in kwargs.items() if value}) + client.update_team(orgname, teamname, new_team) + return click.secho( + "The team %s has been successfully updated." % teamname, + fg="green", + ) diff --git a/platformio/account/validate.py b/platformio/account/validate.py new file mode 100644 index 00000000..69efb753 --- /dev/null +++ b/platformio/account/validate.py @@ -0,0 +1,79 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +import click + + +def validate_username(value, field="username"): + value = str(value).strip() + if not re.match(r"^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,37}$", value, flags=re.I): + raise click.BadParameter( + "Invalid %s format. " + "%s must contain only alphanumeric characters " + "or single hyphens, cannot begin or end with a hyphen, " + "and must not be longer than 38 characters." + % (field.lower(), field.capitalize()) + ) + return value + + +def validate_email(value): + value = str(value).strip() + if not re.match(r"^[a-z\d_.+-]+@[a-z\d\-]+\.[a-z\d\-.]+$", value, flags=re.I): + raise click.BadParameter("Invalid email address") + return value + + +def validate_password(value): + value = str(value).strip() + if not re.match(r"^(?=.*[a-z])(?=.*\d).{8,}$", value): + raise click.BadParameter( + "Invalid password format. " + "Password must contain at least 8 characters" + " including a number and a lowercase letter" + ) + return value + + +def validate_orgname(value): + return validate_username(value, "Organization name") + + +def validate_orgname_teamname(value, teamname_validate=False): + if ":" not in value: + raise click.BadParameter( + "Please specify organization and team name in the next" + " format - orgname:teamname. For example, mycompany:DreamTeam" + ) + teamname = str(value.strip().split(":", 1)[1]) + if teamname_validate: + validate_teamname(teamname) + return value + + +def validate_teamname(value): + if not value: + return value + value = str(value).strip() + if not re.match(r"^[a-z\d](?:[a-z\d]|[\-_ ](?=[a-z\d])){0,19}$", value, flags=re.I): + raise click.BadParameter( + "Invalid team name format. " + "Team name must only contain alphanumeric characters, " + "single hyphens, underscores, spaces. It can not " + "begin or end with a hyphen or a underscore and must" + " not be longer than 20 characters." + ) + return value diff --git a/platformio/builder/main.py b/platformio/builder/main.py index 616f4a0a..6aeee4f4 100644 --- a/platformio/builder/main.py +++ b/platformio/builder/main.py @@ -225,7 +225,7 @@ if "envdump" in COMMAND_LINE_TARGETS: click.echo(env.Dump()) env.Exit(0) -if set(["_idedata", "idedata"]) & set(COMMAND_LINE_TARGETS): +if env.IsIntegrationDump(): projenv = None try: Import("projenv") diff --git a/platformio/builder/tools/pioino.py b/platformio/builder/tools/pioino.py index 7a95da9b..0c1f59a2 100644 --- a/platformio/builder/tools/pioino.py +++ b/platformio/builder/tools/pioino.py @@ -225,11 +225,15 @@ class InoToCPPConverter(object): return "\n".join(result) -def ConvertInoToCpp(env): +def FindInoNodes(env): src_dir = glob.escape(env.subst("$PROJECT_SRC_DIR")) - ino_nodes = env.Glob(os.path.join(src_dir, "*.ino")) + env.Glob( + return env.Glob(os.path.join(src_dir, "*.ino")) + env.Glob( os.path.join(src_dir, "*.pde") ) + + +def ConvertInoToCpp(env): + ino_nodes = env.FindInoNodes() if not ino_nodes: return c = InoToCPPConverter(env) @@ -247,6 +251,7 @@ def _delete_file(path): def generate(env): + env.AddMethod(FindInoNodes) env.AddMethod(ConvertInoToCpp) diff --git a/platformio/builder/tools/piointegration.py b/platformio/builder/tools/piointegration.py index 36989f06..f776bccf 100644 --- a/platformio/builder/tools/piointegration.py +++ b/platformio/builder/tools/piointegration.py @@ -19,10 +19,15 @@ import os import SCons.Defaults # pylint: disable=import-error import SCons.Subst # pylint: disable=import-error +from SCons.Script import COMMAND_LINE_TARGETS # pylint: disable=import-error from platformio.proc import exec_command, where_is_program +def IsIntegrationDump(_): + return set(["_idedata", "idedata"]) & set(COMMAND_LINE_TARGETS) + + def DumpIntegrationIncludes(env): result = dict(build=[], compatlib=[], toolchain=[]) @@ -60,7 +65,7 @@ def DumpIntegrationIncludes(env): return result -def _get_gcc_defines(env): +def get_gcc_defines(env): items = [] try: sysenv = os.environ.copy() @@ -83,7 +88,7 @@ def _get_gcc_defines(env): return items -def _dump_defines(env): +def dump_defines(env): defines = [] # global symbols for item in SCons.Defaults.processDefines(env.get("CPPDEFINES", [])): @@ -108,12 +113,12 @@ def _dump_defines(env): # built-in GCC marcos # if env.GetCompilerType() == "gcc": - # defines.extend(_get_gcc_defines(env)) + # defines.extend(get_gcc_defines(env)) return defines -def _get_svd_path(env): +def dump_svd_path(env): svd_path = env.GetProjectOption("debug_svd_path") if svd_path: return os.path.abspath(svd_path) @@ -146,13 +151,13 @@ def DumpIntegrationData(env, globalenv): data = { "env_name": env["PIOENV"], "libsource_dirs": [env.subst(item) for item in env.GetLibSourceDirs()], - "defines": _dump_defines(env), + "defines": dump_defines(env), "includes": env.DumpIntegrationIncludes(), "cc_path": where_is_program(env.subst("$CC"), env.subst("${ENV['PATH']}")), "cxx_path": where_is_program(env.subst("$CXX"), env.subst("${ENV['PATH']}")), "gdb_path": where_is_program(env.subst("$GDB"), env.subst("${ENV['PATH']}")), "prog_path": env.subst("$PROG_PATH"), - "svd_path": _get_svd_path(env), + "svd_path": dump_svd_path(env), "compiler_type": env.GetCompilerType(), "targets": globalenv.DumpTargets(), "extra": dict( @@ -162,7 +167,9 @@ def DumpIntegrationData(env, globalenv): ] ), } - data["extra"].update(env.get("IDE_EXTRA_DATA", {})) + data["extra"].update( + env.get("INTEGRATION_EXTRA_DATA", env.get("IDE_EXTRA_DATA", {})) + ) env_ = env.Clone() # https://github.com/platformio/platformio-atom-ide/issues/34 @@ -191,6 +198,7 @@ def exists(_): def generate(env): + env.AddMethod(IsIntegrationDump) env.AddMethod(DumpIntegrationIncludes) env.AddMethod(DumpIntegrationData) return env diff --git a/platformio/builder/tools/piolib.py b/platformio/builder/tools/piolib.py index 2372c3de..104e8496 100644 --- a/platformio/builder/tools/piolib.py +++ b/platformio/builder/tools/piolib.py @@ -31,8 +31,8 @@ from SCons.Script import DefaultEnvironment # pylint: disable=import-error from platformio import exception, fs, util from platformio.builder.tools import platformio as piotool -from platformio.clients.http import HTTPClientError, InternetIsOffline from platformio.compat import IS_WINDOWS, hashlib_encode_data, string_types +from platformio.http import HTTPClientError, InternetIsOffline from platformio.package.exception import ( MissingPackageManifestError, UnknownPackageError, @@ -60,16 +60,16 @@ class LibBuilderFactory(object): elif used_frameworks: clsname = "%sLibBuilder" % used_frameworks[0].capitalize() - obj = getattr(sys.modules[__name__], clsname)(env, path, verbose=verbose) + obj = globals()[clsname](env, path, verbose=verbose) # Handle PlatformIOLibBuilder.manifest.build.builder # pylint: disable=protected-access if isinstance(obj, PlatformIOLibBuilder) and obj._manifest.get("build", {}).get( "builder" ): - obj = getattr( - sys.modules[__name__], obj._manifest.get("build", {}).get("builder") - )(env, path, verbose=verbose) + obj = globals()[obj._manifest.get("build", {}).get("builder")]( + env, path, verbose=verbose + ) assert isinstance(obj, LibBuilderBase) return obj @@ -111,7 +111,7 @@ class LibBuilderFactory(object): return [] -class LibBuilderBase(object): +class LibBuilderBase: CLASSIC_SCANNER = SCons.Scanner.C.CScanner() CCONDITIONAL_SCANNER = SCons.Scanner.C.CConditionalScanner() @@ -453,11 +453,17 @@ class LibBuilderBase(object): def build(self): libs = [] + shared_scopes = ("CPPPATH", "LIBPATH", "LIBS", "LINKFLAGS") for lb in self.depbuilders: libs.extend(lb.build()) # copy shared information to self env - for key in ("CPPPATH", "LIBPATH", "LIBS", "LINKFLAGS"): - self.env.PrependUnique(**{key: lb.env.get(key)}) + self.env.PrependUnique( + **{ + scope: lb.env.get(scope) + for scope in shared_scopes + if lb.env.get(scope) + } + ) for lb in self._circular_deps: self.env.PrependUnique(CPPPATH=lb.get_include_dirs()) @@ -472,8 +478,13 @@ class LibBuilderBase(object): for lb in self.env.GetLibBuilders(): if self == lb or not lb.is_built: continue - for key in ("CPPPATH", "LIBPATH", "LIBS", "LINKFLAGS"): - self.env.PrependUnique(**{key: lb.env.get(key)}) + self.env.PrependUnique( + **{ + scope: lb.env.get(scope) + for scope in shared_scopes + if lb.env.get(scope) + } + ) do_not_archive = not self.lib_archive if not do_not_archive: @@ -514,7 +525,7 @@ class ArduinoLibBuilder(LibBuilderBase): return os.path.join(self.path, "include") def get_include_dirs(self): - include_dirs = LibBuilderBase.get_include_dirs(self) + include_dirs = super().get_include_dirs() if os.path.isdir(os.path.join(self.path, "src")): return include_dirs if os.path.isdir(os.path.join(self.path, "utility")): @@ -612,7 +623,7 @@ class MbedLibBuilder(LibBuilderBase): return LibBuilderBase.src_dir.fget(self) # pylint: disable=no-member def get_include_dirs(self): - include_dirs = LibBuilderBase.get_include_dirs(self) + include_dirs = super().get_include_dirs() if self.path not in include_dirs: include_dirs.append(self.path) @@ -833,7 +844,7 @@ class PlatformIOLibBuilder(LibBuilderBase): return util.items_in_list(frameworks, self._manifest.get("frameworks") or ["*"]) def get_include_dirs(self): - include_dirs = LibBuilderBase.get_include_dirs(self) + include_dirs = super().get_include_dirs() # backwards compatibility with PlatformIO 2.0 if ( @@ -872,14 +883,14 @@ class ProjectAsLibBuilder(LibBuilderBase): project_include_dir = self.env.subst("$PROJECT_INCLUDE_DIR") if os.path.isdir(project_include_dir): include_dirs.append(project_include_dir) - for include_dir in LibBuilderBase.get_include_dirs(self): + for include_dir in super().get_include_dirs(): if include_dir not in include_dirs: include_dirs.append(include_dir) return include_dirs def get_search_files(self): # project files - items = LibBuilderBase.get_search_files(self) + items = super().get_search_files() # test files if "test" in self.env.GetBuildType(): items.extend( @@ -994,7 +1005,7 @@ class ProjectAsLibBuilder(LibBuilderBase): def build(self): self.is_built = True # do not build Project now - result = LibBuilderBase.build(self) + result = super().build() self.env.PrependUnique(CPPPATH=self.get_include_dirs()) return result @@ -1027,14 +1038,15 @@ def IsCompatibleLibBuilder(env, lb, verbose=int(ARGUMENTS.get("PIOVERBOSE", 0))) return True -def GetLibBuilders(env): # pylint: disable=too-many-branches - if DefaultEnvironment().get("__PIO_LIB_BUILDERS", None) is not None: +def GetLibBuilders(_): # pylint: disable=too-many-branches + env = DefaultEnvironment() + if env.get("__PIO_LIB_BUILDERS", None) is not None: return sorted( - DefaultEnvironment()["__PIO_LIB_BUILDERS"], + env["__PIO_LIB_BUILDERS"], key=lambda lb: 0 if lb.is_dependent else 1, ) - DefaultEnvironment().Replace(__PIO_LIB_BUILDERS=[]) + env.Replace(__PIO_LIB_BUILDERS=[]) verbose = int(ARGUMENTS.get("PIOVERBOSE", 0)) found_incompat = False @@ -1060,13 +1072,13 @@ def GetLibBuilders(env): # pylint: disable=too-many-branches ) continue if env.IsCompatibleLibBuilder(lb): - DefaultEnvironment().Append(__PIO_LIB_BUILDERS=[lb]) + env.Append(__PIO_LIB_BUILDERS=[lb]) else: found_incompat = True for lb in env.get("EXTRA_LIB_BUILDERS", []): if env.IsCompatibleLibBuilder(lb): - DefaultEnvironment().Append(__PIO_LIB_BUILDERS=[lb]) + env.Append(__PIO_LIB_BUILDERS=[lb]) else: found_incompat = True @@ -1077,7 +1089,7 @@ def GetLibBuilders(env): # pylint: disable=too-many-branches "ldf-compat-mode\n" ) - return DefaultEnvironment()["__PIO_LIB_BUILDERS"] + return env["__PIO_LIB_BUILDERS"] def ConfigureProjectLibBuilder(env): diff --git a/platformio/builder/tools/piomisc.py b/platformio/builder/tools/piomisc.py index 75fec40a..76ec2e37 100644 --- a/platformio/builder/tools/piomisc.py +++ b/platformio/builder/tools/piomisc.py @@ -114,7 +114,15 @@ def ConfigureDebugTarget(env): ] if optimization_flags: - env.AppendUnique(ASFLAGS=optimization_flags, LINKFLAGS=optimization_flags) + env.AppendUnique( + ASFLAGS=[ + # skip -O flags for assembler + f + for f in optimization_flags + if f.startswith("-g") + ], + LINKFLAGS=optimization_flags, + ) def GetExtraScripts(env, scope): diff --git a/platformio/builder/tools/platformio.py b/platformio/builder/tools/platformio.py index fde67426..69875a70 100644 --- a/platformio/builder/tools/platformio.py +++ b/platformio/builder/tools/platformio.py @@ -143,6 +143,7 @@ def ProcessProgramDeps(env): def ProcessProjectDeps(env): project_lib_builder = env.ConfigureProjectLibBuilder() + projenv = project_lib_builder.env # prepend project libs to the beginning of list env.Prepend(LIBS=project_lib_builder.build()) @@ -155,17 +156,18 @@ def ProcessProjectDeps(env): } ) - projenv = env.Clone() - - # CPPPATH from dependencies - projenv.PrependUnique(CPPPATH=project_lib_builder.env.get("CPPPATH")) - # extra build flags from `platformio.ini` - projenv.ProcessFlags(env.get("SRC_BUILD_FLAGS")) - if "test" in env.GetBuildType(): + build_files_before_nums = len(env.get("PIOBUILDFILES", [])) projenv.BuildSources( "$BUILD_TEST_DIR", "$PROJECT_TEST_DIR", "$PIOTEST_SRC_FILTER" ) + if len(env.get("PIOBUILDFILES", [])) - build_files_before_nums < 1: + sys.stderr.write( + "Error: Nothing to build. Please put your test suites " + "to the '%s' folder\n" % env.subst("$PROJECT_TEST_DIR") + ) + env.Exit(1) + if "test" not in env.GetBuildType() or env.GetProjectOption("test_build_src"): projenv.BuildSources( "$BUILD_SRC_DIR", "$PROJECT_SRC_DIR", env.get("SRC_FILTER") @@ -174,7 +176,7 @@ def ProcessProjectDeps(env): if not env.get("PIOBUILDFILES") and not COMMAND_LINE_TARGETS: sys.stderr.write( "Error: Nothing to build. Please put your source code files " - "to '%s' folder\n" % env.subst("$PROJECT_SRC_DIR") + "to the '%s' folder\n" % env.subst("$PROJECT_SRC_DIR") ) env.Exit(1) @@ -327,25 +329,18 @@ def BuildFrameworks(env, frameworks): ) env.Exit(1) - board_frameworks = env.BoardConfig().get("frameworks", []) - if frameworks == ["platformio"]: - if board_frameworks: - frameworks.insert(0, board_frameworks[0]) - else: - sys.stderr.write("Error: Please specify `board` in `platformio.ini`\n") - env.Exit(1) - - for f in frameworks: - if f == "arduino": - # Arduino IDE appends .o the end of filename + supported_frameworks = env.BoardConfig().get("frameworks", []) + for name in frameworks: + if name == "arduino": + # Arduino IDE appends .o to the end of filename Builder.match_splitext = scons_patched_match_splitext if "nobuild" not in COMMAND_LINE_TARGETS: env.ConvertInoToCpp() - if f in board_frameworks: - SConscript(env.GetFrameworkScript(f), exports="env") + if name in supported_frameworks: + SConscript(env.GetFrameworkScript(name), exports="env") else: - sys.stderr.write("Error: This board doesn't support %s framework!\n" % f) + sys.stderr.write("Error: This board doesn't support %s framework!\n" % name) env.Exit(1) diff --git a/platformio/commands/remote/ac/__init__.py b/platformio/check/__init__.py similarity index 100% rename from platformio/commands/remote/ac/__init__.py rename to platformio/check/__init__.py diff --git a/platformio/commands/check/command.py b/platformio/check/cli.py similarity index 98% rename from platformio/commands/check/command.py rename to platformio/check/cli.py index e24836cc..3033008e 100644 --- a/platformio/commands/check/command.py +++ b/platformio/check/cli.py @@ -26,8 +26,8 @@ import click from tabulate import tabulate from platformio import app, exception, fs, util -from platformio.commands.check.defect import DefectItem -from platformio.commands.check.tools import CheckToolFactory +from platformio.check.defect import DefectItem +from platformio.check.tools import CheckToolFactory from platformio.project.config import ProjectConfig from platformio.project.helpers import find_project_dir_above, get_project_dir diff --git a/platformio/commands/check/defect.py b/platformio/check/defect.py similarity index 100% rename from platformio/commands/check/defect.py rename to platformio/check/defect.py diff --git a/platformio/commands/check/tools/__init__.py b/platformio/check/tools/__init__.py similarity index 83% rename from platformio/commands/check/tools/__init__.py rename to platformio/check/tools/__init__.py index 9c4b1b7e..824df5bb 100644 --- a/platformio/commands/check/tools/__init__.py +++ b/platformio/check/tools/__init__.py @@ -13,9 +13,9 @@ # limitations under the License. from platformio import exception -from platformio.commands.check.tools.clangtidy import ClangtidyCheckTool -from platformio.commands.check.tools.cppcheck import CppcheckCheckTool -from platformio.commands.check.tools.pvsstudio import PvsStudioCheckTool +from platformio.check.tools.clangtidy import ClangtidyCheckTool +from platformio.check.tools.cppcheck import CppcheckCheckTool +from platformio.check.tools.pvsstudio import PvsStudioCheckTool class CheckToolFactory(object): diff --git a/platformio/commands/check/tools/base.py b/platformio/check/tools/base.py similarity index 99% rename from platformio/commands/check/tools/base.py rename to platformio/check/tools/base.py index 07636e1f..90c58bc1 100644 --- a/platformio/commands/check/tools/base.py +++ b/platformio/check/tools/base.py @@ -19,7 +19,7 @@ import tempfile import click from platformio import fs, proc -from platformio.commands.check.defect import DefectItem +from platformio.check.defect import DefectItem from platformio.package.manager.core import get_core_package_dir from platformio.package.meta import PackageSpec from platformio.project.helpers import load_build_metadata diff --git a/platformio/commands/check/tools/clangtidy.py b/platformio/check/tools/clangtidy.py similarity index 96% rename from platformio/commands/check/tools/clangtidy.py rename to platformio/check/tools/clangtidy.py index c357cf4d..682a2cb7 100644 --- a/platformio/commands/check/tools/clangtidy.py +++ b/platformio/check/tools/clangtidy.py @@ -15,8 +15,8 @@ import re from os.path import join -from platformio.commands.check.defect import DefectItem -from platformio.commands.check.tools.base import CheckToolBase +from platformio.check.defect import DefectItem +from platformio.check.tools.base import CheckToolBase class ClangtidyCheckTool(CheckToolBase): diff --git a/platformio/commands/check/tools/cppcheck.py b/platformio/check/tools/cppcheck.py similarity index 98% rename from platformio/commands/check/tools/cppcheck.py rename to platformio/check/tools/cppcheck.py index ec2b96d3..0f8db402 100644 --- a/platformio/commands/check/tools/cppcheck.py +++ b/platformio/check/tools/cppcheck.py @@ -17,8 +17,8 @@ import os import click from platformio import proc -from platformio.commands.check.defect import DefectItem -from platformio.commands.check.tools.base import CheckToolBase +from platformio.check.defect import DefectItem +from platformio.check.tools.base import CheckToolBase class CppcheckCheckTool(CheckToolBase): diff --git a/platformio/commands/check/tools/pvsstudio.py b/platformio/check/tools/pvsstudio.py similarity index 98% rename from platformio/commands/check/tools/pvsstudio.py rename to platformio/check/tools/pvsstudio.py index 0479d91e..ded65d1c 100644 --- a/platformio/commands/check/tools/pvsstudio.py +++ b/platformio/check/tools/pvsstudio.py @@ -20,8 +20,8 @@ from xml.etree.ElementTree import fromstring import click from platformio import proc -from platformio.commands.check.defect import DefectItem -from platformio.commands.check.tools.base import CheckToolBase +from platformio.check.defect import DefectItem +from platformio.check.tools.base import CheckToolBase from platformio.compat import IS_WINDOWS diff --git a/platformio/cli.py b/platformio/cli.py new file mode 100644 index 00000000..bdb46a3d --- /dev/null +++ b/platformio/cli.py @@ -0,0 +1,96 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +from pathlib import Path + +import click + + +class PlatformioCLI(click.MultiCommand): + + leftover_args = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._pio_root_path = Path(__file__).parent + self._pio_cmd_aliases = dict(package="pkg") + + def _find_pio_commands(self): + def _to_module_path(p): + return ( + "platformio." + ".".join(p.relative_to(self._pio_root_path).parts)[:-3] + ) + + result = {} + for p in self._pio_root_path.rglob("cli.py"): + # skip this module + if p.parent == self._pio_root_path: + continue + cmd_name = p.parent.name + result[self._pio_cmd_aliases.get(cmd_name, cmd_name)] = _to_module_path(p) + + # find legacy commands + for p in (self._pio_root_path / "commands").iterdir(): + if p.name.startswith("_"): + continue + if (p / "command.py").is_file(): + result[p.name] = _to_module_path(p / "command.py") + elif p.name.endswith(".py"): + result[p.name[:-3]] = _to_module_path(p) + + return result + + @staticmethod + def in_silence(): + args = PlatformioCLI.leftover_args + return args and any( + [ + args[0] == "debug" and "--interpreter" in " ".join(args), + args[0] == "upgrade", + "--json-output" in args, + "--version" in args, + ] + ) + + def invoke(self, ctx): + PlatformioCLI.leftover_args = ctx.args + if hasattr(ctx, "protected_args"): + PlatformioCLI.leftover_args = ctx.protected_args + ctx.args + return super().invoke(ctx) + + def list_commands(self, ctx): + return sorted(list(self._find_pio_commands())) + + def get_command(self, ctx, cmd_name): + commands = self._find_pio_commands() + if cmd_name not in commands: + return self._handle_obsolate_command(ctx, cmd_name) + module = importlib.import_module(commands[cmd_name]) + return getattr(module, "cli") + + @staticmethod + def _handle_obsolate_command(ctx, cmd_name): + # pylint: disable=import-outside-toplevel + if cmd_name == "init": + from platformio.project.commands.init import project_init_cmd + + return project_init_cmd + + if cmd_name == "package": + from platformio.package.cli import cli + + return cli + + raise click.UsageError('No such command "%s"' % cmd_name, ctx) diff --git a/platformio/commands/__init__.py b/platformio/commands/__init__.py index 22cacc60..b0514903 100644 --- a/platformio/commands/__init__.py +++ b/platformio/commands/__init__.py @@ -11,76 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -import os - -import click - - -class PlatformioCLI(click.MultiCommand): - - leftover_args = [] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._pio_cmds_dir = os.path.dirname(__file__) - - @staticmethod - def in_silence(): - args = PlatformioCLI.leftover_args - return args and any( - [ - args[0] == "debug" and "--interpreter" in " ".join(args), - args[0] == "upgrade", - "--json-output" in args, - "--version" in args, - ] - ) - - def invoke(self, ctx): - PlatformioCLI.leftover_args = ctx.args - if hasattr(ctx, "protected_args"): - PlatformioCLI.leftover_args = ctx.protected_args + ctx.args - return super().invoke(ctx) - - def list_commands(self, ctx): - cmds = [] - for cmd_name in os.listdir(self._pio_cmds_dir): - if cmd_name.startswith("__init__"): - continue - if os.path.isfile(os.path.join(self._pio_cmds_dir, cmd_name, "command.py")): - cmds.append(cmd_name) - elif cmd_name.endswith(".py"): - cmds.append(cmd_name[:-3]) - cmds.sort() - return cmds - - def get_command(self, ctx, cmd_name): - mod = None - try: - mod_path = "platformio.commands." + cmd_name - if os.path.isfile(os.path.join(self._pio_cmds_dir, cmd_name, "command.py")): - mod_path = "platformio.commands.%s.command" % cmd_name - mod = __import__(mod_path, None, None, ["cli"]) - except ImportError: - try: - return self._handle_obsolate_command(cmd_name) - except AttributeError: - pass - raise click.UsageError('No such command "%s"' % cmd_name, ctx) - return mod.cli - - @staticmethod - def _handle_obsolate_command(name): - # pylint: disable=import-outside-toplevel - if name == "init": - from platformio.project.commands.init import project_init_cmd - - return project_init_cmd - - if name == "package": - from platformio.commands.pkg import cli - - return cli - - raise AttributeError() diff --git a/platformio/commands/access.py b/platformio/commands/access.py deleted file mode 100644 index 74f48e2b..00000000 --- a/platformio/commands/access.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright (c) 2014-present PlatformIO -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=unused-argument - -import json -import re - -import click -from tabulate import tabulate - -from platformio.clients.registry import RegistryClient -from platformio.commands.account import validate_username -from platformio.commands.team import validate_orgname_teamname - - -def validate_client(value): - if ":" in value: - validate_orgname_teamname(value) - else: - validate_username(value) - return value - - -@click.group("access", short_help="Manage resource access") -def cli(): - pass - - -def validate_urn(value): - value = str(value).strip() - if not re.match(r"^prn:reg:pkg:(\d+):(\w+)$", value, flags=re.I): - raise click.BadParameter("Invalid URN format.") - return value - - -@cli.command("public", short_help="Make resource public") -@click.argument( - "urn", - callback=lambda _, __, value: validate_urn(value), -) -@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") -def access_public(urn, urn_type): - client = RegistryClient() - client.update_resource(urn=urn, private=0) - return click.secho( - "The resource %s has been successfully updated." % urn, - fg="green", - ) - - -@cli.command("private", short_help="Make resource private") -@click.argument( - "urn", - callback=lambda _, __, value: validate_urn(value), -) -@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") -def access_private(urn, urn_type): - client = RegistryClient() - client.update_resource(urn=urn, private=1) - return click.secho( - "The resource %s has been successfully updated." % urn, - fg="green", - ) - - -@cli.command("grant", short_help="Grant access") -@click.argument("level", type=click.Choice(["admin", "maintainer", "guest"])) -@click.argument( - "client", - metavar="[|]", - callback=lambda _, __, value: validate_client(value), -) -@click.argument( - "urn", - callback=lambda _, __, value: validate_urn(value), -) -@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") -def access_grant(level, client, urn, urn_type): - reg_client = RegistryClient() - reg_client.grant_access_for_resource(urn=urn, client=client, level=level) - return click.secho( - "Access for resource %s has been granted for %s" % (urn, client), - fg="green", - ) - - -@cli.command("revoke", short_help="Revoke access") -@click.argument( - "client", - metavar="[ORGNAME:TEAMNAME|USERNAME]", - callback=lambda _, __, value: validate_client(value), -) -@click.argument( - "urn", - callback=lambda _, __, value: validate_urn(value), -) -@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") -def access_revoke(client, urn, urn_type): - reg_client = RegistryClient() - reg_client.revoke_access_from_resource(urn=urn, client=client) - return click.secho( - "Access for resource %s has been revoked for %s" % (urn, client), - fg="green", - ) - - -@cli.command("list", short_help="List published resources") -@click.argument("owner", required=False) -@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") -@click.option("--json-output", is_flag=True) -def access_list(owner, urn_type, json_output): - reg_client = RegistryClient() - resources = reg_client.list_resources(owner=owner) - if json_output: - return click.echo(json.dumps(resources)) - if not resources: - return click.secho("You do not have any resources.", fg="yellow") - for resource in resources: - click.echo() - click.secho(resource.get("name"), fg="cyan") - click.echo("-" * len(resource.get("name"))) - table_data = [] - table_data.append(("URN:", resource.get("urn"))) - table_data.append(("Owner:", resource.get("owner"))) - table_data.append( - ( - "Access:", - click.style("Private", fg="red") - if resource.get("private", False) - else "Public", - ) - ) - table_data.append( - ( - "Access level(s):", - ", ".join( - (level.capitalize() for level in resource.get("access_levels")) - ), - ) - ) - click.echo(tabulate(table_data, tablefmt="plain")) - return click.echo() diff --git a/platformio/commands/account.py b/platformio/commands/account.py deleted file mode 100644 index 48b26717..00000000 --- a/platformio/commands/account.py +++ /dev/null @@ -1,292 +0,0 @@ -# Copyright (c) 2014-present PlatformIO -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=unused-argument - -import json -import re - -import click -from tabulate import tabulate - -from platformio import util -from platformio.clients.account import AccountClient, AccountNotAuthorized - - -@click.group("account", short_help="Manage PlatformIO account") -def cli(): - pass - - -def validate_username(value, field="username"): - value = str(value).strip() - if not re.match(r"^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,37}$", value, flags=re.I): - raise click.BadParameter( - "Invalid %s format. " - "%s must contain only alphanumeric characters " - "or single hyphens, cannot begin or end with a hyphen, " - "and must not be longer than 38 characters." - % (field.lower(), field.capitalize()) - ) - return value - - -def validate_email(value): - value = str(value).strip() - if not re.match(r"^[a-z\d_.+-]+@[a-z\d\-]+\.[a-z\d\-.]+$", value, flags=re.I): - raise click.BadParameter("Invalid email address") - return value - - -def validate_password(value): - value = str(value).strip() - if not re.match(r"^(?=.*[a-z])(?=.*\d).{8,}$", value): - raise click.BadParameter( - "Invalid password format. " - "Password must contain at least 8 characters" - " including a number and a lowercase letter" - ) - return value - - -@cli.command("register", short_help="Create new PlatformIO Account") -@click.option( - "-u", - "--username", - prompt=True, - callback=lambda _, __, value: validate_username(value), -) -@click.option( - "-e", "--email", prompt=True, callback=lambda _, __, value: validate_email(value) -) -@click.option( - "-p", - "--password", - prompt=True, - hide_input=True, - confirmation_prompt=True, - callback=lambda _, __, value: validate_password(value), -) -@click.option("--firstname", prompt=True) -@click.option("--lastname", prompt=True) -def account_register(username, email, password, firstname, lastname): - client = AccountClient() - client.registration(username, email, password, firstname, lastname) - return click.secho( - "An account has been successfully created. " - "Please check your mail to activate your account and verify your email address.", - fg="green", - ) - - -@cli.command("login", short_help="Log in to PlatformIO Account") -@click.option("-u", "--username", prompt="Username or email") -@click.option("-p", "--password", prompt=True, hide_input=True) -def account_login(username, password): - client = AccountClient() - client.login(username, password) - return click.secho("Successfully logged in!", fg="green") - - -@cli.command("logout", short_help="Log out of PlatformIO Account") -def account_logout(): - client = AccountClient() - client.logout() - return click.secho("Successfully logged out!", fg="green") - - -@cli.command("password", short_help="Change password") -@click.option("--old-password", prompt=True, hide_input=True) -@click.option("--new-password", prompt=True, hide_input=True, confirmation_prompt=True) -def account_password(old_password, new_password): - client = AccountClient() - client.change_password(old_password, new_password) - return click.secho("Password successfully changed!", fg="green") - - -@cli.command("token", short_help="Get or regenerate Authentication Token") -@click.option("-p", "--password", prompt=True, hide_input=True) -@click.option("--regenerate", is_flag=True) -@click.option("--json-output", is_flag=True) -def account_token(password, regenerate, json_output): - client = AccountClient() - auth_token = client.auth_token(password, regenerate) - if json_output: - return click.echo(json.dumps({"status": "success", "result": auth_token})) - return click.secho("Personal Authentication Token: %s" % auth_token, fg="green") - - -@cli.command("forgot", short_help="Forgot password") -@click.option("--username", prompt="Username or email") -def account_forgot(username): - client = AccountClient() - client.forgot_password(username) - return click.secho( - "If this account is registered, we will send the " - "further instructions to your email.", - fg="green", - ) - - -@cli.command("update", short_help="Update profile information") -@click.option("--current-password", prompt=True, hide_input=True) -@click.option("--username") -@click.option("--email") -@click.option("--firstname") -@click.option("--lastname") -def account_update(current_password, **kwargs): - client = AccountClient() - profile = client.get_profile() - new_profile = profile.copy() - if not any(kwargs.values()): - for field in profile: - new_profile[field] = click.prompt( - field.replace("_", " ").capitalize(), default=profile[field] - ) - if field == "email": - validate_email(new_profile[field]) - if field == "username": - validate_username(new_profile[field]) - else: - new_profile.update({key: value for key, value in kwargs.items() if value}) - client.update_profile(new_profile, current_password) - click.secho("Profile successfully updated!", fg="green") - username_changed = new_profile["username"] != profile["username"] - email_changed = new_profile["email"] != profile["email"] - if not username_changed and not email_changed: - return None - try: - client.logout() - except AccountNotAuthorized: - pass - if email_changed: - return click.secho( - "Please check your mail to verify your new email address and re-login. ", - fg="yellow", - ) - return click.secho("Please re-login.", fg="yellow") - - -@cli.command("destroy", short_help="Destroy account") -def account_destroy(): - client = AccountClient() - click.confirm( - "Are you sure you want to delete the %s user account?\n" - "Warning! All linked data will be permanently removed and can not be restored." - % client.get_logged_username(), - abort=True, - ) - client.destroy_account() - try: - client.logout() - except AccountNotAuthorized: - pass - return click.secho( - "User account has been destroyed.", - fg="green", - ) - - -@cli.command("show", short_help="PlatformIO Account information") -@click.option("--offline", is_flag=True) -@click.option("--json-output", is_flag=True) -def account_show(offline, json_output): - client = AccountClient() - info = client.get_account_info(offline) - if json_output: - return click.echo(json.dumps(info)) - click.echo() - if info.get("profile"): - print_profile(info["profile"]) - if info.get("packages"): - print_packages(info["packages"]) - if info.get("subscriptions"): - print_subscriptions(info["subscriptions"]) - return click.echo() - - -def print_profile(profile): - click.secho("Profile", fg="cyan", bold=True) - click.echo("=" * len("Profile")) - data = [] - if profile.get("username"): - data.append(("Username:", profile["username"])) - if profile.get("email"): - data.append(("Email:", profile["email"])) - if profile.get("firstname"): - data.append(("First name:", profile["firstname"])) - if profile.get("lastname"): - data.append(("Last name:", profile["lastname"])) - click.echo(tabulate(data, tablefmt="plain")) - - -def print_packages(packages): - click.echo() - click.secho("Packages", fg="cyan") - click.echo("=" * len("Packages")) - for package in packages: - click.echo() - click.secho(package.get("name"), bold=True) - click.echo("-" * len(package.get("name"))) - if package.get("description"): - click.echo(package.get("description")) - data = [] - expire = "-" - if "subscription" in package: - expire = util.parse_datetime( - package["subscription"].get("end_at") - or package["subscription"].get("next_bill_at") - ).strftime("%Y-%m-%d") - data.append(("Expire:", expire)) - services = [] - for key in package: - if not key.startswith("service."): - continue - if isinstance(package[key], dict): - services.append(package[key].get("title")) - else: - services.append(package[key]) - if services: - data.append(("Services:", ", ".join(services))) - click.echo(tabulate(data, tablefmt="plain")) - - -def print_subscriptions(subscriptions): - click.echo() - click.secho("Subscriptions", fg="cyan") - click.echo("=" * len("Subscriptions")) - for subscription in subscriptions: - click.echo() - click.secho(subscription.get("product_name"), bold=True) - click.echo("-" * len(subscription.get("product_name"))) - data = [("State:", subscription.get("status"))] - begin_at = util.parse_datetime(subscription.get("begin_at")).strftime("%c") - data.append(("Start date:", begin_at or "-")) - end_at = subscription.get("end_at") - if end_at: - end_at = util.parse_datetime(subscription.get("end_at")).strftime("%c") - data.append(("End date:", end_at or "-")) - next_bill_at = subscription.get("next_bill_at") - if next_bill_at: - next_bill_at = util.parse_datetime( - subscription.get("next_bill_at") - ).strftime("%c") - data.append(("Next payment:", next_bill_at or "-")) - data.append( - ("Edit:", click.style(subscription.get("update_url"), fg="blue") or "-") - ) - data.append( - ("Cancel:", click.style(subscription.get("cancel_url"), fg="blue") or "-") - ) - click.echo(tabulate(data, tablefmt="plain")) diff --git a/platformio/commands/ci.py b/platformio/commands/ci.py index 17880196..f1875b23 100644 --- a/platformio/commands/ci.py +++ b/platformio/commands/ci.py @@ -20,10 +20,10 @@ import tempfile import click from platformio import app, fs -from platformio.commands.run.command import cli as cmd_run from platformio.exception import CIBuildEnvsEmpty from platformio.project.commands.init import project_init_cmd, validate_boards from platformio.project.config import ProjectConfig +from platformio.run.cli import cli as cmd_run def validate_path(ctx, param, value): # pylint: disable=unused-argument diff --git a/platformio/commands/lib/command.py b/platformio/commands/lib/command.py index 7e3f08e1..3b9af87e 100644 --- a/platformio/commands/lib/command.py +++ b/platformio/commands/lib/command.py @@ -24,7 +24,7 @@ import click from tabulate import tabulate from platformio import exception, fs, util -from platformio.commands import PlatformioCLI +from platformio.cli import PlatformioCLI from platformio.commands.lib.helpers import get_builtin_libs, save_project_libdeps from platformio.package.exception import NotGlobalLibDir, UnknownPackageError from platformio.package.manager.library import LibraryPackageManager @@ -69,13 +69,6 @@ def get_project_global_lib_dir(): @click.pass_context def cli(ctx, **options): in_silence = PlatformioCLI.in_silence() - if not in_silence: - click.secho( - "\nWARNING!!! This command is deprecated and will be removed in " - "the next releases. \nPlease use `pio pkg` instead.\n", - fg="yellow", - ) - storage_cmds = ("install", "uninstall", "update", "list") # skip commands that don't need storage folder if ctx.invoked_subcommand not in storage_cmds or ( @@ -148,6 +141,11 @@ def cli(ctx, **options): def lib_install( # pylint: disable=too-many-arguments,unused-argument ctx, libraries, save, silent, interactive, force ): + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg install` instead.\n", + fg="yellow", + ) storage_dirs = ctx.meta[CTX_META_STORAGE_DIRS_KEY] storage_libdeps = ctx.meta.get(CTX_META_STORAGE_LIBDEPS_KEY, []) @@ -211,6 +209,11 @@ def _save_deps(ctx, pkgs, action="add"): @click.option("-s", "--silent", is_flag=True, help="Suppress progress reporting") @click.pass_context def lib_uninstall(ctx, libraries, save, silent): + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg uninstall` instead.\n", + fg="yellow", + ) storage_dirs = ctx.meta[CTX_META_STORAGE_DIRS_KEY] uninstalled_pkgs = {} for storage_dir in storage_dirs: @@ -246,6 +249,13 @@ def lib_update( # pylint: disable=too-many-arguments "This command is deprecated, please use `pio pkg outdated` instead" ) + if not json_output: + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg update` instead.\n", + fg="yellow", + ) + storage_dirs = ctx.meta[CTX_META_STORAGE_DIRS_KEY] json_result = {} for storage_dir in storage_dirs: @@ -305,6 +315,12 @@ def lib_update( # pylint: disable=too-many-arguments @click.option("--json-output", is_flag=True) @click.pass_context def lib_list(ctx, json_output): + if not json_output: + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg list` instead.\n", + fg="yellow", + ) storage_dirs = ctx.meta[CTX_META_STORAGE_DIRS_KEY] json_result = {} for storage_dir in storage_dirs: @@ -348,6 +364,12 @@ def lib_list(ctx, json_output): help="Do not prompt, automatically paginate with delay", ) def lib_search(query, json_output, page, noninteractive, **filters): + if not json_output: + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg search` instead.\n", + fg="yellow", + ) regclient = LibraryPackageManager().get_registry_client_instance() if not query: query = [] @@ -444,6 +466,12 @@ def lib_builtin(storage, json_output): @click.argument("library", metavar="[LIBRARY]") @click.option("--json-output", is_flag=True) def lib_show(library, json_output): + if not json_output: + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg show` instead.\n", + fg="yellow", + ) lm = LibraryPackageManager() lm.set_log_level(logging.ERROR if json_output else logging.DEBUG) lib_id = lm.reveal_registry_package_id(library) diff --git a/platformio/commands/org.py b/platformio/commands/org.py deleted file mode 100644 index 7f003cb0..00000000 --- a/platformio/commands/org.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright (c) 2014-present PlatformIO -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=unused-argument - -import json - -import click -from tabulate import tabulate - -from platformio.clients.account import AccountClient -from platformio.commands.account import validate_email, validate_username - - -@click.group("org", short_help="Manage organizations") -def cli(): - pass - - -def validate_orgname(value): - return validate_username(value, "Organization name") - - -@cli.command("create", short_help="Create a new organization") -@click.argument( - "orgname", - callback=lambda _, __, value: validate_orgname(value), -) -@click.option( - "--email", callback=lambda _, __, value: validate_email(value) if value else value -) -@click.option( - "--displayname", -) -def org_create(orgname, email, displayname): - client = AccountClient() - client.create_org(orgname, email, displayname) - return click.secho( - "The organization `%s` has been successfully created." % orgname, - fg="green", - ) - - -@cli.command("list", short_help="List organizations and their members") -@click.option("--json-output", is_flag=True) -def org_list(json_output): - client = AccountClient() - orgs = client.list_orgs() - if json_output: - return click.echo(json.dumps(orgs)) - if not orgs: - return click.echo("You do not have any organization") - for org in orgs: - click.echo() - click.secho(org.get("orgname"), fg="cyan") - click.echo("-" * len(org.get("orgname"))) - data = [] - if org.get("displayname"): - data.append(("Display Name:", org.get("displayname"))) - if org.get("email"): - data.append(("Email:", org.get("email"))) - data.append( - ( - "Owners:", - ", ".join((owner.get("username") for owner in org.get("owners"))), - ) - ) - click.echo(tabulate(data, tablefmt="plain")) - return click.echo() - - -@cli.command("update", short_help="Update organization") -@click.argument("cur_orgname") -@click.option( - "--orgname", - callback=lambda _, __, value: validate_orgname(value), - help="A new orgname", -) -@click.option("--email") -@click.option("--displayname") -def org_update(cur_orgname, **kwargs): - client = AccountClient() - org = client.get_org(cur_orgname) - del org["owners"] - new_org = org.copy() - if not any(kwargs.values()): - for field in org: - new_org[field] = click.prompt( - field.replace("_", " ").capitalize(), default=org[field] - ) - if field == "email": - validate_email(new_org[field]) - if field == "orgname": - validate_orgname(new_org[field]) - else: - new_org.update( - {key.replace("new_", ""): value for key, value in kwargs.items() if value} - ) - client.update_org(cur_orgname, new_org) - return click.secho( - "The organization `%s` has been successfully updated." % cur_orgname, - fg="green", - ) - - -@cli.command("destroy", short_help="Destroy organization") -@click.argument("orgname") -def account_destroy(orgname): - client = AccountClient() - click.confirm( - "Are you sure you want to delete the `%s` organization account?\n" - "Warning! All linked data will be permanently removed and can not be restored." - % orgname, - abort=True, - ) - client.destroy_org(orgname) - return click.secho( - "Organization `%s` has been destroyed." % orgname, - fg="green", - ) - - -@cli.command("add", short_help="Add a new owner to organization") -@click.argument( - "orgname", -) -@click.argument( - "username", -) -def org_add_owner(orgname, username): - client = AccountClient() - client.add_org_owner(orgname, username) - return click.secho( - "The new owner `%s` has been successfully added to the `%s` organization." - % (username, orgname), - fg="green", - ) - - -@cli.command("remove", short_help="Remove an owner from organization") -@click.argument( - "orgname", -) -@click.argument( - "username", -) -def org_remove_owner(orgname, username): - client = AccountClient() - client.remove_org_owner(orgname, username) - return click.secho( - "The `%s` owner has been successfully removed from the `%s` organization." - % (username, orgname), - fg="green", - ) diff --git a/platformio/commands/platform.py b/platformio/commands/platform.py index ae80566e..d9e8af2d 100644 --- a/platformio/commands/platform.py +++ b/platformio/commands/platform.py @@ -18,7 +18,6 @@ import os import click -from platformio.commands import PlatformioCLI from platformio.commands.boards import print_boards from platformio.exception import UserSideException from platformio.package.exception import UnknownPackageError @@ -31,18 +30,19 @@ from platformio.platform.factory import PlatformFactory @click.group(short_help="Platform manager", hidden=True) def cli(): - if not PlatformioCLI.in_silence(): - click.secho( - "\nWARNING!!! This command is deprecated and will be removed in " - "the next releases. \nPlease use `pio pkg` instead.\n", - fg="yellow", - ) + pass @cli.command("search", short_help="Search for development platform") @click.argument("query", required=False) @click.option("--json-output", is_flag=True) def platform_search(query, json_output): + if not json_output: + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg search` instead.\n", + fg="yellow", + ) platforms = [] for platform in _get_registry_platforms(): if query == "all": @@ -94,6 +94,12 @@ def platform_frameworks(query, json_output): @cli.command("list", short_help="List installed development platforms") @click.option("--json-output", is_flag=True) def platform_list(json_output): + if not json_output: + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg list` instead.\n", + fg="yellow", + ) platforms = [] pm = PlatformPackageManager() for pkg in pm.get_installed(): @@ -112,6 +118,12 @@ def platform_list(json_output): @click.argument("platform") @click.option("--json-output", is_flag=True) def platform_show(platform, json_output): # pylint: disable=too-many-branches + if not json_output: + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg show` instead.\n", + fg="yellow", + ) data = _get_platform_data(platform) if not data: raise UnknownPlatform(platform) @@ -195,6 +207,12 @@ def platform_install( # pylint: disable=too-many-arguments,too-many-locals silent, force, ): + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg install` instead.\n", + fg="yellow", + ) + def _find_pkg_names(p, candidates): result = [] for candidate in candidates: @@ -251,6 +269,11 @@ def platform_install( # pylint: disable=too-many-arguments,too-many-locals @cli.command("uninstall", short_help="Uninstall development platform") @click.argument("platforms", nargs=-1, required=True, metavar="[PLATFORM...]") def platform_uninstall(platforms): + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg uninstall` instead.\n", + fg="yellow", + ) pm = PlatformPackageManager() pm.set_log_level(logging.DEBUG) for platform in platforms: @@ -285,6 +308,13 @@ def platform_update( # pylint: disable=too-many-locals, too-many-arguments "This command is deprecated, please use `pio pkg outdated` instead" ) + if not json_output: + click.secho( + "\nWARNING: This command is deprecated and will be removed in " + "the next releases. \nPlease use `pio pkg update` instead.\n", + fg="yellow", + ) + pm = PlatformPackageManager() pm.set_log_level(logging.WARN if silent else logging.DEBUG) platforms = platforms or pm.get_installed() diff --git a/platformio/commands/system/command.py b/platformio/commands/system/command.py deleted file mode 100644 index a75007d6..00000000 --- a/platformio/commands/system/command.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright (c) 2014-present PlatformIO -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import platform -import sys - -import click -from tabulate import tabulate - -from platformio import __version__, compat, fs, proc, util -from platformio.commands.system.completion import ( - ShellType, - get_completion_install_path, - install_completion_code, - uninstall_completion_code, -) -from platformio.commands.system.prune import ( - prune_cached_data, - prune_core_packages, - prune_platform_packages, -) -from platformio.package.manager.library import LibraryPackageManager -from platformio.package.manager.platform import PlatformPackageManager -from platformio.package.manager.tool import ToolPackageManager -from platformio.project.config import ProjectConfig - - -@click.group("system", short_help="Miscellaneous system commands") -def cli(): - pass - - -@cli.command("info", short_help="Display system-wide information") -@click.option("--json-output", is_flag=True) -def system_info(json_output): - project_config = ProjectConfig() - data = {} - data["core_version"] = {"title": "PlatformIO Core", "value": __version__} - data["python_version"] = { - "title": "Python", - "value": "{0}.{1}.{2}-{3}.{4}".format(*list(sys.version_info)), - } - data["system"] = {"title": "System Type", "value": util.get_systype()} - data["platform"] = {"title": "Platform", "value": platform.platform(terse=True)} - data["filesystem_encoding"] = { - "title": "File System Encoding", - "value": compat.get_filesystem_encoding(), - } - data["locale_encoding"] = { - "title": "Locale Encoding", - "value": compat.get_locale_encoding(), - } - data["core_dir"] = { - "title": "PlatformIO Core Directory", - "value": project_config.get("platformio", "core_dir"), - } - data["platformio_exe"] = { - "title": "PlatformIO Core Executable", - "value": proc.where_is_program( - "platformio.exe" if compat.IS_WINDOWS else "platformio" - ), - } - data["python_exe"] = { - "title": "Python Executable", - "value": proc.get_pythonexe_path(), - } - data["global_lib_nums"] = { - "title": "Global Libraries", - "value": len(LibraryPackageManager().get_installed()), - } - data["dev_platform_nums"] = { - "title": "Development Platforms", - "value": len(PlatformPackageManager().get_installed()), - } - data["package_tool_nums"] = { - "title": "Tools & Toolchains", - "value": len( - ToolPackageManager( - project_config.get("platformio", "packages_dir") - ).get_installed() - ), - } - - click.echo( - json.dumps(data) - if json_output - else tabulate([(item["title"], item["value"]) for item in data.values()]) - ) - - -@cli.command("prune", short_help="Remove unused data") -@click.option("--force", "-f", is_flag=True, help="Do not prompt for confirmation") -@click.option( - "--dry-run", is_flag=True, help="Do not prune, only show data that will be removed" -) -@click.option("--cache", is_flag=True, help="Prune only cached data") -@click.option( - "--core-packages", is_flag=True, help="Prune only unnecessary core packages" -) -@click.option( - "--platform-packages", - is_flag=True, - help="Prune only unnecessary development platform packages", -) -def system_prune(force, dry_run, cache, core_packages, platform_packages): - if dry_run: - click.secho( - "Dry run mode (do not prune, only show data that will be removed)", - fg="yellow", - ) - click.echo() - - reclaimed_cache = 0 - reclaimed_core_packages = 0 - reclaimed_platform_packages = 0 - prune_all = not any([cache, core_packages, platform_packages]) - - if cache or prune_all: - reclaimed_cache = prune_cached_data(force, dry_run) - click.echo() - - if core_packages or prune_all: - reclaimed_core_packages = prune_core_packages(force, dry_run) - click.echo() - - if platform_packages or prune_all: - reclaimed_platform_packages = prune_platform_packages(force, dry_run) - click.echo() - - click.secho( - "Total reclaimed space: %s" - % fs.humanize_file_size( - reclaimed_cache + reclaimed_core_packages + reclaimed_platform_packages - ), - fg="green", - ) - - -@cli.group("completion", short_help="Shell completion support") -def completion(): - pass - - -@completion.command("install", short_help="Install shell completion files/code") -@click.argument("shell", type=click.Choice([t.value for t in ShellType])) -@click.option( - "--path", - type=click.Path(file_okay=True, dir_okay=False, readable=True, resolve_path=True), - help="Custom installation path of the code to be evaluated by the shell. " - "The standard installation path is used by default.", -) -def completion_install(shell, path): - shell = ShellType(shell) - path = path or get_completion_install_path(shell) - install_completion_code(shell, path) - click.echo( - "PlatformIO CLI completion has been installed for %s shell to %s \n" - "Please restart a current shell session." - % (click.style(shell.name, fg="cyan"), click.style(path, fg="blue")) - ) - - -@completion.command("uninstall", short_help="Uninstall shell completion files/code") -@click.argument("shell", type=click.Choice([t.value for t in ShellType])) -@click.option( - "--path", - type=click.Path(file_okay=True, dir_okay=False, readable=True, resolve_path=True), - help="Custom installation path of the code to be evaluated by the shell. " - "The standard installation path is used by default.", -) -def completion_uninstall(shell, path): - shell = ShellType(shell) - path = path or get_completion_install_path(shell) - uninstall_completion_code(shell, path) - click.echo( - "PlatformIO CLI completion has been uninstalled for %s shell from %s \n" - "Please restart a current shell session." - % (click.style(shell.name, fg="cyan"), click.style(path, fg="blue")) - ) diff --git a/platformio/commands/team.py b/platformio/commands/team.py deleted file mode 100644 index 6733a420..00000000 --- a/platformio/commands/team.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright (c) 2014-present PlatformIO -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# pylint: disable=unused-argument - -import json -import re - -import click -from tabulate import tabulate - -from platformio.clients.account import AccountClient - - -def validate_orgname_teamname(value, teamname_validate=False): - if ":" not in value: - raise click.BadParameter( - "Please specify organization and team name in the next" - " format - orgname:teamname. For example, mycompany:DreamTeam" - ) - teamname = str(value.strip().split(":", 1)[1]) - if teamname_validate: - validate_teamname(teamname) - return value - - -def validate_teamname(value): - if not value: - return value - value = str(value).strip() - if not re.match(r"^[a-z\d](?:[a-z\d]|[\-_ ](?=[a-z\d])){0,19}$", value, flags=re.I): - raise click.BadParameter( - "Invalid team name format. " - "Team name must only contain alphanumeric characters, " - "single hyphens, underscores, spaces. It can not " - "begin or end with a hyphen or a underscore and must" - " not be longer than 20 characters." - ) - return value - - -@click.group("team", short_help="Manage organization teams") -def cli(): - pass - - -@cli.command("create", short_help="Create a new team") -@click.argument( - "orgname_teamname", - metavar="ORGNAME:TEAMNAME", - callback=lambda _, __, value: validate_orgname_teamname( - value, teamname_validate=True - ), -) -@click.option( - "--description", -) -def team_create(orgname_teamname, description): - orgname, teamname = orgname_teamname.split(":", 1) - client = AccountClient() - client.create_team(orgname, teamname, description) - return click.secho( - "The team %s has been successfully created." % teamname, - fg="green", - ) - - -@cli.command("list", short_help="List teams") -@click.argument("orgname", required=False) -@click.option("--json-output", is_flag=True) -def team_list(orgname, json_output): - client = AccountClient() - data = {} - if not orgname: - for item in client.list_orgs(): - teams = client.list_teams(item.get("orgname")) - data[item.get("orgname")] = teams - else: - teams = client.list_teams(orgname) - data[orgname] = teams - if json_output: - return click.echo(json.dumps(data[orgname] if orgname else data)) - if not any(data.values()): - return click.secho("You do not have any teams.", fg="yellow") - for org_name in data: - for team in data[org_name]: - click.echo() - click.secho("%s:%s" % (org_name, team.get("name")), fg="cyan") - click.echo("-" * len("%s:%s" % (org_name, team.get("name")))) - table_data = [] - if team.get("description"): - table_data.append(("Description:", team.get("description"))) - table_data.append( - ( - "Members:", - ", ".join( - (member.get("username") for member in team.get("members")) - ) - if team.get("members") - else "-", - ) - ) - click.echo(tabulate(table_data, tablefmt="plain")) - return click.echo() - - -@cli.command("update", short_help="Update team") -@click.argument( - "orgname_teamname", - metavar="ORGNAME:TEAMNAME", - callback=lambda _, __, value: validate_orgname_teamname(value), -) -@click.option( - "--name", - callback=lambda _, __, value: validate_teamname(value), - help="A new team name", -) -@click.option( - "--description", -) -def team_update(orgname_teamname, **kwargs): - orgname, teamname = orgname_teamname.split(":", 1) - client = AccountClient() - team = client.get_team(orgname, teamname) - del team["id"] - del team["members"] - new_team = team.copy() - if not any(kwargs.values()): - for field in team: - new_team[field] = click.prompt( - field.replace("_", " ").capitalize(), default=team[field] - ) - if field == "name": - validate_teamname(new_team[field]) - else: - new_team.update({key: value for key, value in kwargs.items() if value}) - client.update_team(orgname, teamname, new_team) - return click.secho( - "The team %s has been successfully updated." % teamname, - fg="green", - ) - - -@cli.command("destroy", short_help="Destroy a team") -@click.argument( - "orgname_teamname", - metavar="ORGNAME:TEAMNAME", - callback=lambda _, __, value: validate_orgname_teamname(value), -) -def team_destroy(orgname_teamname): - orgname, teamname = orgname_teamname.split(":", 1) - click.confirm( - click.style( - "Are you sure you want to destroy the %s team?" % teamname, fg="yellow" - ), - abort=True, - ) - client = AccountClient() - client.destroy_team(orgname, teamname) - return click.secho( - "The team %s has been successfully destroyed." % teamname, - fg="green", - ) - - -@cli.command("add", short_help="Add a new member to team") -@click.argument( - "orgname_teamname", - metavar="ORGNAME:TEAMNAME", - callback=lambda _, __, value: validate_orgname_teamname(value), -) -@click.argument( - "username", -) -def team_add_member(orgname_teamname, username): - orgname, teamname = orgname_teamname.split(":", 1) - client = AccountClient() - client.add_team_member(orgname, teamname, username) - return click.secho( - "The new member %s has been successfully added to the %s team." - % (username, teamname), - fg="green", - ) - - -@cli.command("remove", short_help="Remove a member from team") -@click.argument( - "orgname_teamname", - metavar="ORGNAME:TEAMNAME", - callback=lambda _, __, value: validate_orgname_teamname(value), -) -@click.argument("username") -def team_remove_owner(orgname_teamname, username): - orgname, teamname = orgname_teamname.split(":", 1) - client = AccountClient() - client.remove_team_member(orgname, teamname, username) - return click.secho( - "The %s member has been successfully removed from the %s team." - % (username, teamname), - fg="green", - ) diff --git a/platformio/commands/upgrade.py b/platformio/commands/upgrade.py index 0cba9e74..7664732a 100644 --- a/platformio/commands/upgrade.py +++ b/platformio/commands/upgrade.py @@ -20,8 +20,8 @@ from zipfile import ZipFile import click from platformio import VERSION, __version__, app, exception -from platformio.clients.http import fetch_remote_content from platformio.compat import IS_WINDOWS +from platformio.http import fetch_remote_content from platformio.package.manager.core import update_core_packages from platformio.proc import exec_command, get_pythonexe_path from platformio.project.helpers import get_project_cache_dir diff --git a/platformio/debug/command.py b/platformio/debug/cli.py similarity index 99% rename from platformio/debug/command.py rename to platformio/debug/cli.py index e8fd6290..01e81f36 100644 --- a/platformio/debug/command.py +++ b/platformio/debug/cli.py @@ -61,7 +61,7 @@ from platformio.project.options import ProjectOptions @click.option("--interface", type=click.Choice(["gdb"])) @click.argument("__unprocessed", nargs=-1, type=click.UNPROCESSED) @click.pass_context -def debug_cmd( +def cli( ctx, project_dir, project_conf, diff --git a/platformio/debug/helpers.py b/platformio/debug/helpers.py index cd87f141..f156cf45 100644 --- a/platformio/debug/helpers.py +++ b/platformio/debug/helpers.py @@ -20,12 +20,12 @@ from fnmatch import fnmatch from hashlib import sha1 from io import BytesIO -from platformio.commands import PlatformioCLI -from platformio.commands.run.command import cli as cmd_run -from platformio.commands.run.command import print_processing_header +from platformio.cli import PlatformioCLI from platformio.compat import IS_WINDOWS, is_bytes from platformio.debug.exception import DebugInvalidOptionsError from platformio.device.list import list_serial_ports +from platformio.run.cli import cli as cmd_run +from platformio.run.cli import print_processing_header from platformio.test.helpers import list_test_names from platformio.test.result import TestSuite from platformio.test.runners.base import TestRunnerOptions diff --git a/platformio/commands/device/command.py b/platformio/device/cli.py similarity index 100% rename from platformio/commands/device/command.py rename to platformio/device/cli.py diff --git a/platformio/exception.py b/platformio/exception.py index 03382a55..5c0b44ea 100644 --- a/platformio/exception.py +++ b/platformio/exception.py @@ -56,7 +56,7 @@ class MissedUdevRules(InvalidUdevRules): MESSAGE = ( "Warning! Please install `99-platformio-udev.rules`. \nMore details: " - "https://docs.platformio.org/page/faq.html#platformio-udev-rules" + "https://docs.platformio.org/en/latest/core/installation/udev-rules.html" ) @@ -65,7 +65,7 @@ class OutdatedUdevRules(InvalidUdevRules): MESSAGE = ( "Warning! Your `{0}` are outdated. Please update or reinstall them." "\nMore details: " - "https://docs.platformio.org/page/faq.html#platformio-udev-rules" + "https://docs.platformio.org/en/latest/core/installation/udev-rules.html" ) diff --git a/platformio/commands/remote/client/__init__.py b/platformio/home/__init__.py similarity index 100% rename from platformio/commands/remote/client/__init__.py rename to platformio/home/__init__.py diff --git a/platformio/commands/home/command.py b/platformio/home/cli.py similarity index 96% rename from platformio/commands/home/command.py rename to platformio/home/cli.py index b656fc07..2d0ba1ce 100644 --- a/platformio/commands/home/command.py +++ b/platformio/home/cli.py @@ -16,8 +16,8 @@ import mimetypes import click -from platformio.commands.home.helpers import is_port_used -from platformio.commands.home.run import run_server +from platformio.home.helpers import is_port_used +from platformio.home.run import run_server @click.command("home", short_help="GUI to manage PlatformIO") diff --git a/platformio/commands/home/helpers.py b/platformio/home/helpers.py similarity index 100% rename from platformio/commands/home/helpers.py rename to platformio/home/helpers.py diff --git a/platformio/commands/remote/factory/__init__.py b/platformio/home/rpc/__init__.py similarity index 100% rename from platformio/commands/remote/factory/__init__.py rename to platformio/home/rpc/__init__.py diff --git a/platformio/commands/run/__init__.py b/platformio/home/rpc/handlers/__init__.py similarity index 100% rename from platformio/commands/run/__init__.py rename to platformio/home/rpc/handlers/__init__.py diff --git a/platformio/commands/home/rpc/handlers/account.py b/platformio/home/rpc/handlers/account.py similarity index 95% rename from platformio/commands/home/rpc/handlers/account.py rename to platformio/home/rpc/handlers/account.py index 5336d600..23777437 100644 --- a/platformio/commands/home/rpc/handlers/account.py +++ b/platformio/home/rpc/handlers/account.py @@ -14,7 +14,7 @@ from ajsonrpc.core import JSONRPC20DispatchException -from platformio.clients.account import AccountClient +from platformio.account.client import AccountClient class AccountRPC: diff --git a/platformio/commands/home/rpc/handlers/app.py b/platformio/home/rpc/handlers/app.py similarity index 100% rename from platformio/commands/home/rpc/handlers/app.py rename to platformio/home/rpc/handlers/app.py diff --git a/platformio/commands/home/rpc/handlers/ide.py b/platformio/home/rpc/handlers/ide.py similarity index 100% rename from platformio/commands/home/rpc/handlers/ide.py rename to platformio/home/rpc/handlers/ide.py diff --git a/platformio/commands/home/rpc/handlers/misc.py b/platformio/home/rpc/handlers/misc.py similarity index 96% rename from platformio/commands/home/rpc/handlers/misc.py rename to platformio/home/rpc/handlers/misc.py index 7626456a..c384fea9 100644 --- a/platformio/commands/home/rpc/handlers/misc.py +++ b/platformio/home/rpc/handlers/misc.py @@ -16,8 +16,8 @@ import json import time from platformio.cache import ContentCache -from platformio.commands.home.rpc.handlers.os import OSRPC from platformio.compat import aio_create_task +from platformio.home.rpc.handlers.os import OSRPC class MiscRPC: diff --git a/platformio/commands/home/rpc/handlers/os.py b/platformio/home/rpc/handlers/os.py similarity index 97% rename from platformio/commands/home/rpc/handlers/os.py rename to platformio/home/rpc/handlers/os.py index 7342b669..1aaf6d63 100644 --- a/platformio/commands/home/rpc/handlers/os.py +++ b/platformio/home/rpc/handlers/os.py @@ -24,9 +24,9 @@ import click from platformio import __default_requests_timeout__, fs from platformio.cache import ContentCache -from platformio.clients.http import ensure_internet_on -from platformio.commands.home import helpers from platformio.device.list import list_logical_devices +from platformio.home import helpers +from platformio.http import ensure_internet_on class OSRPC: diff --git a/platformio/commands/home/rpc/handlers/piocore.py b/platformio/home/rpc/handlers/piocore.py similarity index 99% rename from platformio/commands/home/rpc/handlers/piocore.py rename to platformio/home/rpc/handlers/piocore.py index 01bbf90d..add05a31 100644 --- a/platformio/commands/home/rpc/handlers/piocore.py +++ b/platformio/home/rpc/handlers/piocore.py @@ -25,8 +25,8 @@ from ajsonrpc.core import JSONRPC20DispatchException from starlette.concurrency import run_in_threadpool from platformio import __main__, __version__, fs, proc -from platformio.commands.home import helpers from platformio.compat import get_locale_encoding, is_bytes +from platformio.home import helpers class MultiThreadingStdStream(object): diff --git a/platformio/commands/home/rpc/handlers/project.py b/platformio/home/rpc/handlers/project.py similarity index 98% rename from platformio/commands/home/rpc/handlers/project.py rename to platformio/home/rpc/handlers/project.py index 8418fa60..13a9ebbb 100644 --- a/platformio/commands/home/rpc/handlers/project.py +++ b/platformio/home/rpc/handlers/project.py @@ -21,8 +21,8 @@ import time from ajsonrpc.core import JSONRPC20DispatchException from platformio import exception, fs -from platformio.commands.home.rpc.handlers.app import AppRPC -from platformio.commands.home.rpc.handlers.piocore import PIOCoreRPC +from platformio.home.rpc.handlers.app import AppRPC +from platformio.home.rpc.handlers.piocore import PIOCoreRPC from platformio.package.manager.platform import PlatformPackageManager from platformio.project.config import ProjectConfig from platformio.project.exception import ProjectError diff --git a/platformio/commands/home/rpc/server.py b/platformio/home/rpc/server.py similarity index 100% rename from platformio/commands/home/rpc/server.py rename to platformio/home/rpc/server.py diff --git a/platformio/commands/home/run.py b/platformio/home/run.py similarity index 86% rename from platformio/commands/home/run.py rename to platformio/home/run.py index 64d820ea..a5306ad0 100644 --- a/platformio/commands/home/run.py +++ b/platformio/home/run.py @@ -24,16 +24,16 @@ from starlette.routing import Mount, Route, WebSocketRoute from starlette.staticfiles import StaticFiles from starlette.status import HTTP_403_FORBIDDEN -from platformio.commands.home.rpc.handlers.account import AccountRPC -from platformio.commands.home.rpc.handlers.app import AppRPC -from platformio.commands.home.rpc.handlers.ide import IDERPC -from platformio.commands.home.rpc.handlers.misc import MiscRPC -from platformio.commands.home.rpc.handlers.os import OSRPC -from platformio.commands.home.rpc.handlers.piocore import PIOCoreRPC -from platformio.commands.home.rpc.handlers.project import ProjectRPC -from platformio.commands.home.rpc.server import WebSocketJSONRPCServerFactory from platformio.compat import aio_get_running_loop from platformio.exception import PlatformioException +from platformio.home.rpc.handlers.account import AccountRPC +from platformio.home.rpc.handlers.app import AppRPC +from platformio.home.rpc.handlers.ide import IDERPC +from platformio.home.rpc.handlers.misc import MiscRPC +from platformio.home.rpc.handlers.os import OSRPC +from platformio.home.rpc.handlers.piocore import PIOCoreRPC +from platformio.home.rpc.handlers.project import ProjectRPC +from platformio.home.rpc.server import WebSocketJSONRPCServerFactory from platformio.package.manager.core import get_core_package_dir from platformio.proc import force_exit diff --git a/platformio/clients/http.py b/platformio/http.py similarity index 99% rename from platformio/clients/http.py rename to platformio/http.py index 86fb8cae..5f5fbdd2 100644 --- a/platformio/clients/http.py +++ b/platformio/http.py @@ -115,7 +115,7 @@ class HTTPClient(object): ) if with_authorization and "Authorization" not in headers: # pylint: disable=import-outside-toplevel - from platformio.clients.account import AccountClient + from platformio.account.client import AccountClient headers["Authorization"] = ( "Bearer %s" % AccountClient().fetch_authentication_token() diff --git a/platformio/maintenance.py b/platformio/maintenance.py index dccd8086..925d2f3a 100644 --- a/platformio/maintenance.py +++ b/platformio/maintenance.py @@ -21,15 +21,15 @@ import semantic_version from platformio import __version__, app, exception, fs, telemetry from platformio.cache import cleanup_content_cache -from platformio.clients import http -from platformio.commands import PlatformioCLI +from platformio.cli import PlatformioCLI from platformio.commands.platform import platform_update as cmd_platform_update -from platformio.commands.system.prune import calculate_unnecessary_system_data from platformio.commands.upgrade import get_latest_version +from platformio.http import HTTPClientError, InternetIsOffline, ensure_internet_on from platformio.package.manager.core import update_core_packages from platformio.package.manager.tool import ToolPackageManager from platformio.package.meta import PackageSpec from platformio.package.version import pepver_to_semver +from platformio.system.prune import calculate_unnecessary_system_data def on_platformio_start(ctx, force, caller): @@ -51,8 +51,8 @@ def on_platformio_end(ctx, result): # pylint: disable=unused-argument check_platformio_upgrade() check_prune_system() except ( - http.HTTPClientError, - http.InternetIsOffline, + HTTPClientError, + InternetIsOffline, exception.GetLatestVersionError, ): click.secho( @@ -142,8 +142,8 @@ def after_upgrade(ctx): ) click.secho("Please remove multiple PIO Cores from a system:", fg="yellow") click.secho( - "https://docs.platformio.org/page/faq.html" - "#multiple-platformio-cores-in-a-system", + "https://docs.platformio.org/en/latest/core" + "/installation/troubleshooting.html", fg="cyan", ) click.secho("*" * terminal_width, fg="yellow") @@ -213,7 +213,7 @@ def check_platformio_upgrade(): if not last_checked_time: return - http.ensure_internet_on(raise_exception=True) + ensure_internet_on(raise_exception=True) # Update PlatformIO Core packages update_core_packages() diff --git a/platformio/commands/pkg.py b/platformio/package/cli.py similarity index 100% rename from platformio/commands/pkg.py rename to platformio/package/cli.py diff --git a/platformio/package/commands/install.py b/platformio/package/commands/install.py index 472d0ae8..8a6e3017 100644 --- a/platformio/package/commands/install.py +++ b/platformio/package/commands/install.py @@ -264,7 +264,8 @@ def _uninstall_project_unused_libdeps(project_env, options): lm.uninstall(spec) except UnknownPackageError: pass - storage_dir.mkdir(parents=True, exist_ok=True) + if not storage_dir.is_dir(): + storage_dir.mkdir(parents=True) integrity_dat.write_text("\n".join(lib_deps), encoding="utf-8") diff --git a/platformio/package/commands/publish.py b/platformio/package/commands/publish.py index 150fdb19..c1c12a5a 100644 --- a/platformio/package/commands/publish.py +++ b/platformio/package/commands/publish.py @@ -21,14 +21,14 @@ import click from tabulate import tabulate from platformio import fs -from platformio.clients.account import AccountClient -from platformio.clients.registry import RegistryClient +from platformio.account.client import AccountClient from platformio.exception import UserSideException from platformio.package.manifest.parser import ManifestParserFactory from platformio.package.manifest.schema import ManifestSchema from platformio.package.meta import PackageType from platformio.package.pack import PackagePacker from platformio.package.unpack import FileUnpacker, TARArchiver +from platformio.registry.client import RegistryClient def validate_datetime(ctx, param, value): # pylint: disable=unused-argument diff --git a/platformio/package/commands/search.py b/platformio/package/commands/search.py index 57ec76ec..ac71ef4c 100644 --- a/platformio/package/commands/search.py +++ b/platformio/package/commands/search.py @@ -17,7 +17,7 @@ import math import click from platformio import util -from platformio.clients.registry import RegistryClient +from platformio.registry.client import RegistryClient @click.command("search", short_help="Search for packages") diff --git a/platformio/package/commands/show.py b/platformio/package/commands/show.py index ce50bc88..82b6e47c 100644 --- a/platformio/package/commands/show.py +++ b/platformio/package/commands/show.py @@ -18,10 +18,10 @@ import click from tabulate import tabulate from platformio import fs, util -from platformio.clients.registry import RegistryClient from platformio.exception import UserSideException from platformio.package.manager._registry import PackageManagerRegistryMixin from platformio.package.meta import PackageSpec, PackageType +from platformio.registry.client import RegistryClient @click.command("show", short_help="Show package information") diff --git a/platformio/package/commands/unpublish.py b/platformio/package/commands/unpublish.py index 3185144e..7d0e633e 100644 --- a/platformio/package/commands/unpublish.py +++ b/platformio/package/commands/unpublish.py @@ -14,9 +14,9 @@ import click -from platformio.clients.account import AccountClient -from platformio.clients.registry import RegistryClient +from platformio.account.client import AccountClient from platformio.package.meta import PackageSpec, PackageType +from platformio.registry.client import RegistryClient @click.command("unpublish", short_help="Remove a pushed package from the registry") diff --git a/platformio/package/manager/_registry.py b/platformio/package/manager/_registry.py index b45a17f2..0a9f39c5 100644 --- a/platformio/package/manager/_registry.py +++ b/platformio/package/manager/_registry.py @@ -12,97 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import time -from urllib.parse import urlparse import click -from platformio import __registry_mirror_hosts__ -from platformio.cache import ContentCache -from platformio.clients.http import HTTPClient -from platformio.clients.registry import RegistryClient from platformio.package.exception import UnknownPackageError from platformio.package.meta import PackageSpec from platformio.package.version import cast_version_to_semver - - -class RegistryFileMirrorIterator(object): - - HTTP_CLIENT_INSTANCES = {} - - def __init__(self, download_url): - self.download_url = download_url - self._url_parts = urlparse(download_url) - self._mirror = "%s://%s" % (self._url_parts.scheme, self._url_parts.netloc) - self._visited_mirrors = [] - - def __iter__(self): # pylint: disable=non-iterator-returned - return self - - def __next__(self): - cache_key = ContentCache.key_from_args( - "head", self.download_url, self._visited_mirrors - ) - with ContentCache("http") as cc: - result = cc.get(cache_key) - if result is not None: - try: - headers = json.loads(result) - return ( - headers["Location"], - headers["X-PIO-Content-SHA256"], - ) - except (ValueError, KeyError): - pass - - http = self.get_http_client() - response = http.send_request( - "head", - self._url_parts.path, - allow_redirects=False, - params=dict(bypass=",".join(self._visited_mirrors)) - if self._visited_mirrors - else None, - x_with_authorization=RegistryClient.allowed_private_packages(), - ) - stop_conditions = [ - response.status_code not in (302, 307), - not response.headers.get("Location"), - not response.headers.get("X-PIO-Mirror"), - response.headers.get("X-PIO-Mirror") in self._visited_mirrors, - ] - if any(stop_conditions): - raise StopIteration - self._visited_mirrors.append(response.headers.get("X-PIO-Mirror")) - cc.set( - cache_key, - json.dumps( - { - "Location": response.headers.get("Location"), - "X-PIO-Content-SHA256": response.headers.get( - "X-PIO-Content-SHA256" - ), - } - ), - "1h", - ) - return ( - response.headers.get("Location"), - response.headers.get("X-PIO-Content-SHA256"), - ) - - def get_http_client(self): - if self._mirror not in RegistryFileMirrorIterator.HTTP_CLIENT_INSTANCES: - endpoints = [self._mirror] - for host in __registry_mirror_hosts__: - endpoint = f"https://dl.{host}" - if endpoint not in endpoints: - endpoints.append(endpoint) - RegistryFileMirrorIterator.HTTP_CLIENT_INSTANCES[self._mirror] = HTTPClient( - endpoints - ) - return RegistryFileMirrorIterator.HTTP_CLIENT_INSTANCES[self._mirror] +from platformio.registry.client import RegistryClient +from platformio.registry.mirror import RegistryFileMirrorIterator class PackageManagerRegistryMixin(object): diff --git a/platformio/package/manager/base.py b/platformio/package/manager/base.py index 07ca2f5b..b692140f 100644 --- a/platformio/package/manager/base.py +++ b/platformio/package/manager/base.py @@ -21,7 +21,7 @@ import click import semantic_version from platformio import fs, util -from platformio.commands import PlatformioCLI +from platformio.cli import PlatformioCLI from platformio.compat import ci_strings_are_equal from platformio.package.exception import ManifestException, MissingPackageManifestError from platformio.package.lockfile import LockFile diff --git a/platformio/package/manager/platform.py b/platformio/package/manager/platform.py index 6d0cc040..41f7b41e 100644 --- a/platformio/package/manager/platform.py +++ b/platformio/package/manager/platform.py @@ -15,7 +15,7 @@ import os from platformio import util -from platformio.clients.http import HTTPClientError, InternetIsOffline +from platformio.http import HTTPClientError, InternetIsOffline from platformio.package.exception import UnknownPackageError from platformio.package.manager.base import BasePackageManager from platformio.package.manager.core import get_installed_core_packages diff --git a/platformio/package/manifest/parser.py b/platformio/package/manifest/parser.py index a5e0f837..8735bcff 100644 --- a/platformio/package/manifest/parser.py +++ b/platformio/package/manifest/parser.py @@ -21,8 +21,8 @@ import tarfile from urllib.parse import urlparse from platformio import util -from platformio.clients.http import fetch_remote_content from platformio.compat import get_object_members, string_types +from platformio.http import fetch_remote_content from platformio.package.exception import ManifestParserError, UnknownManifestError from platformio.project.helpers import is_platformio_project diff --git a/platformio/package/manifest/schema.py b/platformio/package/manifest/schema.py index c8f69e0c..8258dfe8 100644 --- a/platformio/package/manifest/schema.py +++ b/platformio/package/manifest/schema.py @@ -22,7 +22,7 @@ import requests import semantic_version from marshmallow import Schema, ValidationError, fields, validate, validates -from platformio.clients.http import fetch_remote_content +from platformio.http import fetch_remote_content from platformio.package.exception import ManifestValidationError from platformio.util import memoized diff --git a/platformio/package/vcsclient.py b/platformio/package/vcsclient.py index ed7434b0..dc4c090d 100644 --- a/platformio/package/vcsclient.py +++ b/platformio/package/vcsclient.py @@ -15,7 +15,6 @@ import os import re import subprocess -import sys from urllib.parse import urlparse from platformio import proc @@ -47,12 +46,12 @@ class VCSClientFactory(object): if not type_: raise VCSBaseException("VCS: Unknown repository type %s" % remote_url) try: - obj = getattr(sys.modules[__name__], "%sClient" % type_.capitalize())( + obj = globals()["%sClient" % type_.capitalize()]( src_dir, remote_url, tag, silent ) assert isinstance(obj, VCSClientBase) return obj - except (AttributeError, AssertionError): + except (KeyError, AssertionError): raise VCSBaseException("VCS: Unknown repository type %s" % remote_url) diff --git a/platformio/platform/factory.py b/platformio/platform/factory.py index 2df9e1b4..0345cb2a 100644 --- a/platformio/platform/factory.py +++ b/platformio/platform/factory.py @@ -14,11 +14,12 @@ import os import re +import sys from platformio import fs from platformio.compat import load_python_module from platformio.package.meta import PackageItem -from platformio.platform.base import PlatformBase +from platformio.platform import base from platformio.platform.exception import UnknownPlatform @@ -29,14 +30,16 @@ class PlatformFactory(object): return "%sPlatform" % name.lower().capitalize() @staticmethod - def load_module(name, path): + def load_platform_module(name, path): + # backward compatibiility with the legacy dev-platforms + sys.modules["platformio.managers.platform"] = base try: return load_python_module("platformio.platform.%s" % name, path) except ImportError: raise UnknownPlatform(name) @classmethod - def new(cls, pkg_or_spec, autoinstall=False) -> PlatformBase: + def new(cls, pkg_or_spec, autoinstall=False) -> base.PlatformBase: # pylint: disable=import-outside-toplevel from platformio.package.manager.platform import PlatformPackageManager @@ -72,16 +75,16 @@ class PlatformFactory(object): platform_cls = None if os.path.isfile(os.path.join(platform_dir, "platform.py")): platform_cls = getattr( - cls.load_module( + cls.load_platform_module( platform_name, os.path.join(platform_dir, "platform.py") ), cls.get_clsname(platform_name), ) else: platform_cls = type( - str(cls.get_clsname(platform_name)), (PlatformBase,), {} + str(cls.get_clsname(platform_name)), (base.PlatformBase,), {} ) _instance = platform_cls(os.path.join(platform_dir, "platform.json")) - assert isinstance(_instance, PlatformBase) + assert isinstance(_instance, base.PlatformBase) return _instance diff --git a/platformio/commands/project.py b/platformio/project/cli.py similarity index 100% rename from platformio/commands/project.py rename to platformio/project/cli.py diff --git a/platformio/project/helpers.py b/platformio/project/helpers.py index bc8d324a..d8fb5822 100644 --- a/platformio/project/helpers.py +++ b/platformio/project/helpers.py @@ -43,15 +43,14 @@ def find_project_dir_above(path): return None -def get_project_all_lib_dirs(): +def get_project_watch_lib_dirs(): """Used by platformio-node-helpers.project.observer.fetchLibDirs""" config = ProjectConfig.get_instance() - libdeps_dir = config.get("platformio", "libdeps_dir") result = [ config.get("platformio", "globallib_dir"), config.get("platformio", "lib_dir"), - libdeps_dir, ] + libdeps_dir = config.get("platformio", "libdeps_dir") if not os.path.isdir(libdeps_dir): return result for d in os.listdir(libdeps_dir): @@ -60,6 +59,9 @@ def get_project_all_lib_dirs(): return result +get_project_all_lib_dirs = get_project_watch_lib_dirs + + def get_project_cache_dir(): """Deprecated, use ProjectConfig.get("platformio", "cache_dir") instead""" return ProjectConfig.get_instance().get("platformio", "cache_dir") @@ -126,7 +128,7 @@ def load_build_metadata(project_dir, env_or_envs, cache=False): env_names = [env_names] with fs.cd(project_dir): - result = _load_cached_project_ide_data(project_dir, env_names) if cache else {} + result = _get_cached_build_metadata(project_dir, env_names) if cache else {} missed_env_names = set(env_names) - set(result.keys()) if missed_env_names: result.update(_load_build_metadata(project_dir, missed_env_names)) @@ -142,7 +144,7 @@ load_project_ide_data = load_build_metadata def _load_build_metadata(project_dir, env_names): # pylint: disable=import-outside-toplevel - from platformio.commands.run.command import cli as cmd_run + from platformio.run.cli import cli as cmd_run args = ["--project-dir", project_dir, "--target", "_idedata"] for name in env_names: @@ -154,10 +156,10 @@ def _load_build_metadata(project_dir, env_names): raise result.exception if '"includes":' not in result.output: raise exception.PlatformioException(result.output) - return _load_cached_project_ide_data(project_dir, env_names) + return _get_cached_build_metadata(project_dir, env_names) -def _load_cached_project_ide_data(project_dir, env_names): +def _get_cached_build_metadata(project_dir, env_names): build_dir = ProjectConfig.get_instance( os.path.join(project_dir, "platformio.ini") ).get("platformio", "build_dir") diff --git a/platformio/project/options.py b/platformio/project/options.py index 02a8e580..ad96b24c 100644 --- a/platformio/project/options.py +++ b/platformio/project/options.py @@ -675,7 +675,8 @@ ProjectOptions = OrderedDict( ConfigEnvOption( group="test", name="test_speed", - description="A connection speed (baud rate) to communicate with a target device", + description="A connection speed (baud rate) to communicate with " + "a target device", type=click.INT, default=115200, ), diff --git a/platformio/public.py b/platformio/public.py index ead093ce..51eecc52 100644 --- a/platformio/public.py +++ b/platformio/public.py @@ -19,7 +19,7 @@ from platformio.device.list import list_serial_ports from platformio.fs import to_unix_path from platformio.platform.base import PlatformBase from platformio.project.config import ProjectConfig -from platformio.project.helpers import load_build_metadata +from platformio.project.helpers import get_project_watch_lib_dirs, load_build_metadata from platformio.test.result import TestCase, TestCaseSource, TestStatus from platformio.test.runners.base import TestRunnerBase from platformio.test.runners.doctest import DoctestTestCaseParser diff --git a/platformio/commands/system/__init__.py b/platformio/registry/__init__.py similarity index 100% rename from platformio/commands/system/__init__.py rename to platformio/registry/__init__.py diff --git a/platformio/managers/__init__.py b/platformio/registry/access/__init__.py similarity index 100% rename from platformio/managers/__init__.py rename to platformio/registry/access/__init__.py diff --git a/platformio/registry/access/cli.py b/platformio/registry/access/cli.py new file mode 100644 index 00000000..fe585c94 --- /dev/null +++ b/platformio/registry/access/cli.py @@ -0,0 +1,36 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.registry.access.commands.grant import access_grant_cmd +from platformio.registry.access.commands.list import access_list_cmd +from platformio.registry.access.commands.private import access_private_cmd +from platformio.registry.access.commands.public import access_public_cmd +from platformio.registry.access.commands.revoke import access_revoke_cmd + + +@click.group( + "access", + commands=[ + access_grant_cmd, + access_list_cmd, + access_private_cmd, + access_public_cmd, + access_revoke_cmd, + ], + short_help="Manage resource access", +) +def cli(): + pass diff --git a/tests/ino2cpp/__init__.py b/platformio/registry/access/commands/__init__.py similarity index 100% rename from tests/ino2cpp/__init__.py rename to platformio/registry/access/commands/__init__.py diff --git a/platformio/registry/access/commands/grant.py b/platformio/registry/access/commands/grant.py new file mode 100644 index 00000000..f9214476 --- /dev/null +++ b/platformio/registry/access/commands/grant.py @@ -0,0 +1,39 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.registry.access.validate import validate_client, validate_urn +from platformio.registry.client import RegistryClient + + +@click.command("grant", short_help="Grant access") +@click.argument("level", type=click.Choice(["admin", "maintainer", "guest"])) +@click.argument( + "client", + metavar="[|]", + callback=lambda _, __, value: validate_client(value), +) +@click.argument( + "urn", + callback=lambda _, __, value: validate_urn(value), +) +@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") +def access_grant_cmd(level, client, urn, urn_type): # pylint: disable=unused-argument + reg_client = RegistryClient() + reg_client.grant_access_for_resource(urn=urn, client=client, level=level) + return click.secho( + "Access for resource %s has been granted for %s" % (urn, client), + fg="green", + ) diff --git a/platformio/registry/access/commands/list.py b/platformio/registry/access/commands/list.py new file mode 100644 index 00000000..ce9d8a6a --- /dev/null +++ b/platformio/registry/access/commands/list.py @@ -0,0 +1,58 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import click +from tabulate import tabulate + +from platformio.registry.client import RegistryClient + + +@click.command("list", short_help="List published resources") +@click.argument("owner", required=False) +@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") +@click.option("--json-output", is_flag=True) +def access_list_cmd(owner, urn_type, json_output): # pylint: disable=unused-argument + reg_client = RegistryClient() + resources = reg_client.list_resources(owner=owner) + if json_output: + return click.echo(json.dumps(resources)) + if not resources: + return click.secho("You do not have any resources.", fg="yellow") + for resource in resources: + click.echo() + click.secho(resource.get("name"), fg="cyan") + click.echo("-" * len(resource.get("name"))) + table_data = [] + table_data.append(("URN:", resource.get("urn"))) + table_data.append(("Owner:", resource.get("owner"))) + table_data.append( + ( + "Access:", + click.style("Private", fg="red") + if resource.get("private", False) + else "Public", + ) + ) + table_data.append( + ( + "Access level(s):", + ", ".join( + (level.capitalize() for level in resource.get("access_levels")) + ), + ) + ) + click.echo(tabulate(table_data, tablefmt="plain")) + return click.echo() diff --git a/platformio/registry/access/commands/private.py b/platformio/registry/access/commands/private.py new file mode 100644 index 00000000..5211aeb3 --- /dev/null +++ b/platformio/registry/access/commands/private.py @@ -0,0 +1,33 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.registry.access.validate import validate_urn +from platformio.registry.client import RegistryClient + + +@click.command("private", short_help="Make resource private") +@click.argument( + "urn", + callback=lambda _, __, value: validate_urn(value), +) +@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") +def access_private_cmd(urn, urn_type): # pylint: disable=unused-argument + client = RegistryClient() + client.update_resource(urn=urn, private=1) + return click.secho( + "The resource %s has been successfully updated." % urn, + fg="green", + ) diff --git a/platformio/registry/access/commands/public.py b/platformio/registry/access/commands/public.py new file mode 100644 index 00000000..465dcf32 --- /dev/null +++ b/platformio/registry/access/commands/public.py @@ -0,0 +1,33 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.registry.access.validate import validate_urn +from platformio.registry.client import RegistryClient + + +@click.command("public", short_help="Make resource public") +@click.argument( + "urn", + callback=lambda _, __, value: validate_urn(value), +) +@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") +def access_public_cmd(urn, urn_type): # pylint: disable=unused-argument + client = RegistryClient() + client.update_resource(urn=urn, private=0) + return click.secho( + "The resource %s has been successfully updated." % urn, + fg="green", + ) diff --git a/platformio/registry/access/commands/revoke.py b/platformio/registry/access/commands/revoke.py new file mode 100644 index 00000000..b6bb0008 --- /dev/null +++ b/platformio/registry/access/commands/revoke.py @@ -0,0 +1,38 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.registry.access.validate import validate_client, validate_urn +from platformio.registry.client import RegistryClient + + +@click.command("revoke", short_help="Revoke access") +@click.argument( + "client", + metavar="[ORGNAME:TEAMNAME|USERNAME]", + callback=lambda _, __, value: validate_client(value), +) +@click.argument( + "urn", + callback=lambda _, __, value: validate_urn(value), +) +@click.option("--urn-type", type=click.Choice(["prn:reg:pkg"]), default="prn:reg:pkg") +def access_revoke_cmd(client, urn, urn_type): # pylint: disable=unused-argument + reg_client = RegistryClient() + reg_client.revoke_access_from_resource(urn=urn, client=client) + return click.secho( + "Access for resource %s has been revoked for %s" % (urn, client), + fg="green", + ) diff --git a/platformio/registry/access/validate.py b/platformio/registry/access/validate.py new file mode 100644 index 00000000..71ca5a92 --- /dev/null +++ b/platformio/registry/access/validate.py @@ -0,0 +1,34 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +import click + +from platformio.account.validate import validate_orgname_teamname, validate_username + + +def validate_urn(value): + value = str(value).strip() + if not re.match(r"^prn:reg:pkg:(\d+):(\w+)$", value, flags=re.I): + raise click.BadParameter("Invalid URN format.") + return value + + +def validate_client(value): + if ":" in value: + validate_orgname_teamname(value) + else: + validate_username(value) + return value diff --git a/platformio/clients/registry.py b/platformio/registry/client.py similarity index 97% rename from platformio/clients/registry.py rename to platformio/registry/client.py index 75bfd99b..2c465e0c 100644 --- a/platformio/clients/registry.py +++ b/platformio/registry/client.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from platformio import __registry_mirror_hosts__, fs -from platformio.clients.account import AccountClient, AccountError -from platformio.clients.http import HTTPClient, HTTPClientError - # pylint: disable=too-many-arguments +from platformio import __registry_mirror_hosts__, fs +from platformio.account.client import AccountClient, AccountError +from platformio.http import HTTPClient, HTTPClientError + class RegistryClient(HTTPClient): def __init__(self): diff --git a/platformio/registry/mirror.py b/platformio/registry/mirror.py new file mode 100644 index 00000000..d967838e --- /dev/null +++ b/platformio/registry/mirror.py @@ -0,0 +1,99 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from urllib.parse import urlparse + +from platformio import __registry_mirror_hosts__ +from platformio.cache import ContentCache +from platformio.http import HTTPClient +from platformio.registry.client import RegistryClient + + +class RegistryFileMirrorIterator(object): + + HTTP_CLIENT_INSTANCES = {} + + def __init__(self, download_url): + self.download_url = download_url + self._url_parts = urlparse(download_url) + self._mirror = "%s://%s" % (self._url_parts.scheme, self._url_parts.netloc) + self._visited_mirrors = [] + + def __iter__(self): # pylint: disable=non-iterator-returned + return self + + def __next__(self): + cache_key = ContentCache.key_from_args( + "head", self.download_url, self._visited_mirrors + ) + with ContentCache("http") as cc: + result = cc.get(cache_key) + if result is not None: + try: + headers = json.loads(result) + return ( + headers["Location"], + headers["X-PIO-Content-SHA256"], + ) + except (ValueError, KeyError): + pass + + http = self.get_http_client() + response = http.send_request( + "head", + self._url_parts.path, + allow_redirects=False, + params=dict(bypass=",".join(self._visited_mirrors)) + if self._visited_mirrors + else None, + x_with_authorization=RegistryClient.allowed_private_packages(), + ) + stop_conditions = [ + response.status_code not in (302, 307), + not response.headers.get("Location"), + not response.headers.get("X-PIO-Mirror"), + response.headers.get("X-PIO-Mirror") in self._visited_mirrors, + ] + if any(stop_conditions): + raise StopIteration + self._visited_mirrors.append(response.headers.get("X-PIO-Mirror")) + cc.set( + cache_key, + json.dumps( + { + "Location": response.headers.get("Location"), + "X-PIO-Content-SHA256": response.headers.get( + "X-PIO-Content-SHA256" + ), + } + ), + "1h", + ) + return ( + response.headers.get("Location"), + response.headers.get("X-PIO-Content-SHA256"), + ) + + def get_http_client(self): + if self._mirror not in RegistryFileMirrorIterator.HTTP_CLIENT_INSTANCES: + endpoints = [self._mirror] + for host in __registry_mirror_hosts__: + endpoint = f"https://dl.{host}" + if endpoint not in endpoints: + endpoints.append(endpoint) + RegistryFileMirrorIterator.HTTP_CLIENT_INSTANCES[self._mirror] = HTTPClient( + endpoints + ) + return RegistryFileMirrorIterator.HTTP_CLIENT_INSTANCES[self._mirror] diff --git a/platformio/commands/test.py b/platformio/remote/__init__.py similarity index 87% rename from platformio/commands/test.py rename to platformio/remote/__init__.py index df641161..b0514903 100644 --- a/platformio/commands/test.py +++ b/platformio/remote/__init__.py @@ -11,7 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -# pylint: disable=unused-import - -from platformio.test.command import test_cmd as cli diff --git a/platformio/commands/debug.py b/platformio/remote/ac/__init__.py similarity index 87% rename from platformio/commands/debug.py rename to platformio/remote/ac/__init__.py index 3ab61d9b..b0514903 100644 --- a/platformio/commands/debug.py +++ b/platformio/remote/ac/__init__.py @@ -11,7 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -# pylint: disable=unused-import - -from platformio.debug.command import debug_cmd as cli diff --git a/platformio/commands/remote/ac/base.py b/platformio/remote/ac/base.py similarity index 100% rename from platformio/commands/remote/ac/base.py rename to platformio/remote/ac/base.py diff --git a/platformio/commands/remote/ac/process.py b/platformio/remote/ac/process.py similarity index 95% rename from platformio/commands/remote/ac/process.py rename to platformio/remote/ac/process.py index 9e4f6989..91da456e 100644 --- a/platformio/commands/remote/ac/process.py +++ b/platformio/remote/ac/process.py @@ -16,7 +16,7 @@ import os from twisted.internet import protocol, reactor # pylint: disable=import-error -from platformio.commands.remote.ac.base import AsyncCommandBase +from platformio.remote.ac.base import AsyncCommandBase class ProcessAsyncCmd(protocol.ProcessProtocol, AsyncCommandBase): diff --git a/platformio/commands/remote/ac/psync.py b/platformio/remote/ac/psync.py similarity index 93% rename from platformio/commands/remote/ac/psync.py rename to platformio/remote/ac/psync.py index 87789ed8..8728737c 100644 --- a/platformio/commands/remote/ac/psync.py +++ b/platformio/remote/ac/psync.py @@ -17,8 +17,8 @@ import os import zlib from io import BytesIO -from platformio.commands.remote.ac.base import AsyncCommandBase -from platformio.commands.remote.projectsync import PROJECT_SYNC_STAGE, ProjectSync +from platformio.remote.ac.base import AsyncCommandBase +from platformio.remote.projectsync import PROJECT_SYNC_STAGE, ProjectSync class ProjectSyncAsyncCmd(AsyncCommandBase): diff --git a/platformio/commands/remote/ac/serial.py b/platformio/remote/ac/serial.py similarity index 96% rename from platformio/commands/remote/ac/serial.py rename to platformio/remote/ac/serial.py index d0181f9c..7f7f83f9 100644 --- a/platformio/commands/remote/ac/serial.py +++ b/platformio/remote/ac/serial.py @@ -17,7 +17,7 @@ from time import sleep from twisted.internet import protocol, reactor # pylint: disable=import-error from twisted.internet.serialport import SerialPort # pylint: disable=import-error -from platformio.commands.remote.ac.base import AsyncCommandBase +from platformio.remote.ac.base import AsyncCommandBase class SerialPortAsyncCmd(protocol.Protocol, AsyncCommandBase): diff --git a/platformio/commands/remote/command.py b/platformio/remote/cli.py similarity index 94% rename from platformio/commands/remote/command.py rename to platformio/remote/cli.py index 5f97d983..1a32c549 100644 --- a/platformio/commands/remote/command.py +++ b/platformio/remote/cli.py @@ -24,7 +24,6 @@ from time import sleep import click from platformio import fs, proc -from platformio.commands.run.command import cli as cmd_run from platformio.device.commands.monitor import ( apply_project_monitor_options, device_monitor_cmd, @@ -34,7 +33,8 @@ from platformio.device.commands.monitor import ( from platformio.package.manager.core import inject_contrib_pysite from platformio.project.exception import NotPlatformIOProjectError from platformio.project.options import ProjectOptions -from platformio.test.command import test_cmd +from platformio.run.cli import cli as cmd_run +from platformio.test.cli import cli as test_cmd @click.group("remote", short_help="Remote Development") @@ -60,14 +60,14 @@ def remote_agent(): type=click.Path(file_okay=False, dir_okay=True, writable=True, resolve_path=True), ) def remote_agent_start(name, share, working_dir): - from platformio.commands.remote.client.agent_service import RemoteAgentService + from platformio.remote.client.agent_service import RemoteAgentService RemoteAgentService(name, share, working_dir).connect() @remote_agent.command("list", short_help="List active agents") def remote_agent_list(): - from platformio.commands.remote.client.agent_list import AgentListClient + from platformio.remote.client.agent_list import AgentListClient AgentListClient().connect() @@ -84,7 +84,7 @@ def remote_agent_list(): ) @click.pass_obj def remote_update(agents, only_check, dry_run): - from platformio.commands.remote.client.update_core import UpdateCoreClient + from platformio.remote.client.update_core import UpdateCoreClient UpdateCoreClient("update", agents, dict(only_check=only_check or dry_run)).connect() @@ -120,7 +120,7 @@ def remote_run( verbose, ): - from platformio.commands.remote.client.run_or_test import RunOrTestClient + from platformio.remote.client.run_or_test import RunOrTestClient cr = RunOrTestClient( "run", @@ -213,7 +213,7 @@ def remote_test( # pylint: disable=redefined-builtin verbose, ): - from platformio.commands.remote.client.run_or_test import RunOrTestClient + from platformio.remote.client.run_or_test import RunOrTestClient cr = RunOrTestClient( "test", @@ -263,7 +263,7 @@ def remote_device(): @click.option("--json-output", is_flag=True) @click.pass_obj def device_list(agents, json_output): - from platformio.commands.remote.client.device_list import DeviceListClient + from platformio.remote.client.device_list import DeviceListClient DeviceListClient(agents, json_output).connect() @@ -346,7 +346,7 @@ def device_list(agents, json_output): @click.pass_obj @click.pass_context def device_monitor(ctx, agents, **kwargs): - from platformio.commands.remote.client.device_monitor import DeviceMonitorClient + from platformio.remote.client.device_monitor import DeviceMonitorClient if kwargs["sock"]: return DeviceMonitorClient(agents, **kwargs).connect() diff --git a/platformio/managers/platform.py b/platformio/remote/client/__init__.py similarity index 81% rename from platformio/managers/platform.py rename to platformio/remote/client/__init__.py index 9ef5e646..b0514903 100644 --- a/platformio/managers/platform.py +++ b/platformio/remote/client/__init__.py @@ -11,6 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -# Backward compatibility with legacy dev-platforms -from platformio.platform.base import PlatformBase # pylint: disable=unused-import diff --git a/platformio/commands/remote/client/agent_list.py b/platformio/remote/client/agent_list.py similarity index 94% rename from platformio/commands/remote/client/agent_list.py rename to platformio/remote/client/agent_list.py index df1de28b..103c0f8c 100644 --- a/platformio/commands/remote/client/agent_list.py +++ b/platformio/remote/client/agent_list.py @@ -16,7 +16,7 @@ from datetime import datetime import click -from platformio.commands.remote.client.base import RemoteClientBase +from platformio.remote.client.base import RemoteClientBase class AgentListClient(RemoteClientBase): diff --git a/platformio/commands/remote/client/agent_service.py b/platformio/remote/client/agent_service.py similarity index 95% rename from platformio/commands/remote/client/agent_service.py rename to platformio/remote/client/agent_service.py index b24f605d..a2f0a05a 100644 --- a/platformio/commands/remote/client/agent_service.py +++ b/platformio/remote/client/agent_service.py @@ -18,13 +18,13 @@ from twisted.logger import LogLevel # pylint: disable=import-error from twisted.spread import pb # pylint: disable=import-error from platformio import proc -from platformio.commands.remote.ac.process import ProcessAsyncCmd -from platformio.commands.remote.ac.psync import ProjectSyncAsyncCmd -from platformio.commands.remote.ac.serial import SerialPortAsyncCmd -from platformio.commands.remote.client.base import RemoteClientBase from platformio.device.list import list_serial_ports from platformio.project.config import ProjectConfig from platformio.project.exception import NotPlatformIOProjectError +from platformio.remote.ac.process import ProcessAsyncCmd +from platformio.remote.ac.psync import ProjectSyncAsyncCmd +from platformio.remote.ac.serial import SerialPortAsyncCmd +from platformio.remote.client.base import RemoteClientBase class RemoteAgentService(RemoteClientBase): @@ -135,7 +135,7 @@ class RemoteAgentService(RemoteClientBase): def _process_cmd_test(self, options): return self._process_cmd_run_or_test("test", options) - def _process_cmd_run_or_test( # pylint: disable=too-many-locals,too-many-branches + def _process_cmd_run_or_test( # pylint: disable=too-many-locals,too-many-branches,too-many-statements self, command, options ): assert options and "project_id" in options @@ -172,6 +172,8 @@ class RemoteAgentService(RemoteClientBase): cmd_args.extend(["-e", env]) for target in options.get("target", []): cmd_args.extend(["-t", target]) + for filter_ in options.get("filter", []): + cmd_args.extend(["-f", filter_]) for ignore in options.get("ignore", []): cmd_args.extend(["-i", ignore]) if options.get("upload_port", False): diff --git a/platformio/commands/remote/client/async_base.py b/platformio/remote/client/async_base.py similarity index 97% rename from platformio/commands/remote/client/async_base.py rename to platformio/remote/client/async_base.py index a07e110b..488802a5 100644 --- a/platformio/commands/remote/client/async_base.py +++ b/platformio/remote/client/async_base.py @@ -15,7 +15,7 @@ import click from twisted.spread import pb # pylint: disable=import-error -from platformio.commands.remote.client.base import RemoteClientBase +from platformio.remote.client.base import RemoteClientBase class AsyncClientBase(RemoteClientBase): diff --git a/platformio/commands/remote/client/base.py b/platformio/remote/client/base.py similarity index 97% rename from platformio/commands/remote/client/base.py rename to platformio/remote/client/base.py index 7ca7be3b..fe2c4fb4 100644 --- a/platformio/commands/remote/client/base.py +++ b/platformio/remote/client/base.py @@ -26,8 +26,8 @@ from twisted.spread import pb # pylint: disable=import-error from zope.interface import provider # pylint: disable=import-error from platformio import __pioremote_endpoint__, __version__, app, exception, maintenance -from platformio.commands.remote.factory.client import RemoteClientFactory -from platformio.commands.remote.factory.ssl import SSLContextFactory +from platformio.remote.factory.client import RemoteClientFactory +from platformio.remote.factory.ssl import SSLContextFactory class RemoteClientBase( # pylint: disable=too-many-instance-attributes diff --git a/platformio/commands/remote/client/device_list.py b/platformio/remote/client/device_list.py similarity index 96% rename from platformio/commands/remote/client/device_list.py rename to platformio/remote/client/device_list.py index dba1729f..a22911b4 100644 --- a/platformio/commands/remote/client/device_list.py +++ b/platformio/remote/client/device_list.py @@ -16,7 +16,7 @@ import json import click -from platformio.commands.remote.client.base import RemoteClientBase +from platformio.remote.client.base import RemoteClientBase class DeviceListClient(RemoteClientBase): diff --git a/platformio/commands/remote/client/device_monitor.py b/platformio/remote/client/device_monitor.py similarity index 99% rename from platformio/commands/remote/client/device_monitor.py rename to platformio/remote/client/device_monitor.py index b880d270..7ddc0048 100644 --- a/platformio/commands/remote/client/device_monitor.py +++ b/platformio/remote/client/device_monitor.py @@ -19,7 +19,7 @@ import click from twisted.internet import protocol, reactor, task # pylint: disable=import-error from twisted.spread import pb # pylint: disable=import-error -from platformio.commands.remote.client.base import RemoteClientBase +from platformio.remote.client.base import RemoteClientBase class SMBridgeProtocol(protocol.Protocol): # pylint: disable=no-init diff --git a/platformio/commands/remote/client/run_or_test.py b/platformio/remote/client/run_or_test.py similarity index 98% rename from platformio/commands/remote/client/run_or_test.py rename to platformio/remote/client/run_or_test.py index cdc1465d..71065640 100644 --- a/platformio/commands/remote/client/run_or_test.py +++ b/platformio/remote/client/run_or_test.py @@ -21,10 +21,10 @@ from io import BytesIO from twisted.spread import pb # pylint: disable=import-error from platformio import fs -from platformio.commands.remote.client.async_base import AsyncClientBase -from platformio.commands.remote.projectsync import PROJECT_SYNC_STAGE, ProjectSync from platformio.compat import hashlib_encode_data from platformio.project.config import ProjectConfig +from platformio.remote.client.async_base import AsyncClientBase +from platformio.remote.projectsync import PROJECT_SYNC_STAGE, ProjectSync class RunOrTestClient(AsyncClientBase): diff --git a/platformio/commands/remote/client/update_core.py b/platformio/remote/client/update_core.py similarity index 92% rename from platformio/commands/remote/client/update_core.py rename to platformio/remote/client/update_core.py index 49e4488c..4fdec4cc 100644 --- a/platformio/commands/remote/client/update_core.py +++ b/platformio/remote/client/update_core.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from platformio.commands.remote.client.async_base import AsyncClientBase +from platformio.remote.client.async_base import AsyncClientBase class UpdateCoreClient(AsyncClientBase): diff --git a/platformio/remote/factory/__init__.py b/platformio/remote/factory/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/platformio/remote/factory/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/platformio/commands/remote/factory/client.py b/platformio/remote/factory/client.py similarity index 98% rename from platformio/commands/remote/factory/client.py rename to platformio/remote/factory/client.py index 712449d5..2565e6ad 100644 --- a/platformio/commands/remote/factory/client.py +++ b/platformio/remote/factory/client.py @@ -16,8 +16,8 @@ from twisted.cred import credentials # pylint: disable=import-error from twisted.internet import defer, protocol, reactor # pylint: disable=import-error from twisted.spread import pb # pylint: disable=import-error +from platformio.account.client import AccountClient from platformio.app import get_host_id -from platformio.clients.account import AccountClient class RemoteClientFactory(pb.PBClientFactory, protocol.ReconnectingClientFactory): diff --git a/platformio/commands/remote/factory/ssl.py b/platformio/remote/factory/ssl.py similarity index 100% rename from platformio/commands/remote/factory/ssl.py rename to platformio/remote/factory/ssl.py diff --git a/platformio/commands/remote/projectsync.py b/platformio/remote/projectsync.py similarity index 100% rename from platformio/commands/remote/projectsync.py rename to platformio/remote/projectsync.py diff --git a/platformio/run/__init__.py b/platformio/run/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/platformio/run/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/platformio/commands/run/command.py b/platformio/run/cli.py similarity index 98% rename from platformio/commands/run/command.py rename to platformio/run/cli.py index 16d71bc9..aa1c0d6c 100644 --- a/platformio/commands/run/command.py +++ b/platformio/run/cli.py @@ -22,11 +22,11 @@ import click from tabulate import tabulate from platformio import app, exception, fs, util -from platformio.commands.run.helpers import clean_build_dir, handle_legacy_libdeps -from platformio.commands.run.processor import EnvironmentProcessor from platformio.device.commands.monitor import device_monitor_cmd from platformio.project.config import ProjectConfig from platformio.project.helpers import find_project_dir_above, load_build_metadata +from platformio.run.helpers import clean_build_dir, handle_legacy_libdeps +from platformio.run.processor import EnvironmentProcessor from platformio.test.runners.base import CTX_META_TEST_IS_RUNNING # pylint: disable=too-many-arguments,too-many-locals,too-many-branches diff --git a/platformio/commands/run/helpers.py b/platformio/run/helpers.py similarity index 100% rename from platformio/commands/run/helpers.py rename to platformio/run/helpers.py diff --git a/platformio/commands/run/processor.py b/platformio/run/processor.py similarity index 100% rename from platformio/commands/run/processor.py rename to platformio/run/processor.py diff --git a/platformio/system/__init__.py b/platformio/system/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/platformio/system/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/platformio/system/cli.py b/platformio/system/cli.py new file mode 100644 index 00000000..dd39ce87 --- /dev/null +++ b/platformio/system/cli.py @@ -0,0 +1,32 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.system.commands.completion import system_completion_cmd +from platformio.system.commands.info import system_info_cmd +from platformio.system.commands.prune import system_prune_cmd + + +@click.group( + "system", + commands=[ + system_completion_cmd, + system_info_cmd, + system_prune_cmd, + ], + short_help="Miscellaneous system commands", +) +def cli(): + pass diff --git a/platformio/system/commands/__init__.py b/platformio/system/commands/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/platformio/system/commands/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/platformio/system/commands/completion.py b/platformio/system/commands/completion.py new file mode 100644 index 00000000..aac68592 --- /dev/null +++ b/platformio/system/commands/completion.py @@ -0,0 +1,69 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio.system.completion import ( + ShellType, + get_completion_install_path, + install_completion_code, + uninstall_completion_code, +) + + +@click.group("completion", short_help="Shell completion support") +def system_completion_cmd(): + pass + + +@system_completion_cmd.command( + "install", short_help="Install shell completion files/code" +) +@click.argument("shell", type=click.Choice([t.value for t in ShellType])) +@click.option( + "--path", + type=click.Path(file_okay=True, dir_okay=False, readable=True, resolve_path=True), + help="Custom installation path of the code to be evaluated by the shell. " + "The standard installation path is used by default.", +) +def system_completion_install(shell, path): + shell = ShellType(shell) + path = path or get_completion_install_path(shell) + install_completion_code(shell, path) + click.echo( + "PlatformIO CLI completion has been installed for %s shell to %s \n" + "Please restart a current shell session." + % (click.style(shell.name, fg="cyan"), click.style(path, fg="blue")) + ) + + +@system_completion_cmd.command( + "uninstall", short_help="Uninstall shell completion files/code" +) +@click.argument("shell", type=click.Choice([t.value for t in ShellType])) +@click.option( + "--path", + type=click.Path(file_okay=True, dir_okay=False, readable=True, resolve_path=True), + help="Custom installation path of the code to be evaluated by the shell. " + "The standard installation path is used by default.", +) +def system_completion_uninstall(shell, path): + shell = ShellType(shell) + path = path or get_completion_install_path(shell) + uninstall_completion_code(shell, path) + click.echo( + "PlatformIO CLI completion has been uninstalled for %s shell from %s \n" + "Please restart a current shell session." + % (click.style(shell.name, fg="cyan"), click.style(path, fg="blue")) + ) diff --git a/platformio/system/commands/info.py b/platformio/system/commands/info.py new file mode 100644 index 00000000..52a06636 --- /dev/null +++ b/platformio/system/commands/info.py @@ -0,0 +1,85 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import json +import platform +import sys + +import click +from tabulate import tabulate + +from platformio import __version__, compat, proc, util +from platformio.package.manager.library import LibraryPackageManager +from platformio.package.manager.platform import PlatformPackageManager +from platformio.package.manager.tool import ToolPackageManager +from platformio.project.config import ProjectConfig + + +@click.command("info", short_help="Display system-wide information") +@click.option("--json-output", is_flag=True) +def system_info_cmd(json_output): + project_config = ProjectConfig() + data = {} + data["core_version"] = {"title": "PlatformIO Core", "value": __version__} + data["python_version"] = { + "title": "Python", + "value": "{0}.{1}.{2}-{3}.{4}".format(*list(sys.version_info)), + } + data["system"] = {"title": "System Type", "value": util.get_systype()} + data["platform"] = {"title": "Platform", "value": platform.platform(terse=True)} + data["filesystem_encoding"] = { + "title": "File System Encoding", + "value": compat.get_filesystem_encoding(), + } + data["locale_encoding"] = { + "title": "Locale Encoding", + "value": compat.get_locale_encoding(), + } + data["core_dir"] = { + "title": "PlatformIO Core Directory", + "value": project_config.get("platformio", "core_dir"), + } + data["platformio_exe"] = { + "title": "PlatformIO Core Executable", + "value": proc.where_is_program( + "platformio.exe" if compat.IS_WINDOWS else "platformio" + ), + } + data["python_exe"] = { + "title": "Python Executable", + "value": proc.get_pythonexe_path(), + } + data["global_lib_nums"] = { + "title": "Global Libraries", + "value": len(LibraryPackageManager().get_installed()), + } + data["dev_platform_nums"] = { + "title": "Development Platforms", + "value": len(PlatformPackageManager().get_installed()), + } + data["package_tool_nums"] = { + "title": "Tools & Toolchains", + "value": len( + ToolPackageManager( + project_config.get("platformio", "packages_dir") + ).get_installed() + ), + } + + click.echo( + json.dumps(data) + if json_output + else tabulate([(item["title"], item["value"]) for item in data.values()]) + ) diff --git a/platformio/system/commands/prune.py b/platformio/system/commands/prune.py new file mode 100644 index 00000000..1559aab3 --- /dev/null +++ b/platformio/system/commands/prune.py @@ -0,0 +1,70 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from platformio import fs +from platformio.system.prune import ( + prune_cached_data, + prune_core_packages, + prune_platform_packages, +) + + +@click.command("prune", short_help="Remove unused data") +@click.option("--force", "-f", is_flag=True, help="Do not prompt for confirmation") +@click.option( + "--dry-run", is_flag=True, help="Do not prune, only show data that will be removed" +) +@click.option("--cache", is_flag=True, help="Prune only cached data") +@click.option( + "--core-packages", is_flag=True, help="Prune only unnecessary core packages" +) +@click.option( + "--platform-packages", + is_flag=True, + help="Prune only unnecessary development platform packages", +) +def system_prune_cmd(force, dry_run, cache, core_packages, platform_packages): + if dry_run: + click.secho( + "Dry run mode (do not prune, only show data that will be removed)", + fg="yellow", + ) + click.echo() + + reclaimed_cache = 0 + reclaimed_core_packages = 0 + reclaimed_platform_packages = 0 + prune_all = not any([cache, core_packages, platform_packages]) + + if cache or prune_all: + reclaimed_cache = prune_cached_data(force, dry_run) + click.echo() + + if core_packages or prune_all: + reclaimed_core_packages = prune_core_packages(force, dry_run) + click.echo() + + if platform_packages or prune_all: + reclaimed_platform_packages = prune_platform_packages(force, dry_run) + click.echo() + + click.secho( + "Total reclaimed space: %s" + % fs.humanize_file_size( + reclaimed_cache + reclaimed_core_packages + reclaimed_platform_packages + ), + fg="green", + ) diff --git a/platformio/commands/system/completion.py b/platformio/system/completion.py similarity index 100% rename from platformio/commands/system/completion.py rename to platformio/system/completion.py diff --git a/platformio/commands/system/prune.py b/platformio/system/prune.py similarity index 100% rename from platformio/commands/system/prune.py rename to platformio/system/prune.py diff --git a/platformio/telemetry.py b/platformio/telemetry.py index ba721792..c0fbf97d 100644 --- a/platformio/telemetry.py +++ b/platformio/telemetry.py @@ -28,7 +28,7 @@ from traceback import format_exc import requests from platformio import __version__, app, exception, util -from platformio.commands import PlatformioCLI +from platformio.cli import PlatformioCLI from platformio.compat import hashlib_encode_data, string_types from platformio.proc import is_ci, is_container from platformio.project.helpers import is_platformio_project diff --git a/platformio/test/command.py b/platformio/test/cli.py similarity index 95% rename from platformio/test/command.py rename to platformio/test/cli.py index cfc2ea3d..bfb14fd6 100644 --- a/platformio/test/command.py +++ b/platformio/test/cli.py @@ -85,9 +85,14 @@ from platformio.test.runners.factory import TestRunnerFactory @click.option("--list-tests", is_flag=True) @click.option("--json-output-path", type=click.Path(resolve_path=True)) @click.option("--junit-output-path", type=click.Path(resolve_path=True)) -@click.option("--verbose", "-v", is_flag=True) +@click.option( + "--verbose", + "-v", + count=True, + help="Increase verbosity level, maximum is 3 levels (-vvv), see docs for details", +) @click.pass_context -def test_cmd( # pylint: disable=too-many-arguments,too-many-locals,redefined-builtin +def cli( # pylint: disable=too-many-arguments,too-many-locals,redefined-builtin ctx, environment, ignore, @@ -121,7 +126,7 @@ def test_cmd( # pylint: disable=too-many-arguments,too-many-locals,redefined-bu test_names = sorted(set(s.test_name for s in test_suites)) if not verbose: - click.echo("Verbose mode can be enabled via `-v, --verbose` option") + click.echo("Verbosity level can be increased via `-v, -vv, or -vvv` option") click.secho("Collected %d tests" % len(test_names), bold=True, nl=not verbose) if verbose: click.echo(" (%s)" % ", ".join(test_names)) diff --git a/platformio/test/exception.py b/platformio/test/exception.py index 2d8c790c..048e7399 100644 --- a/platformio/test/exception.py +++ b/platformio/test/exception.py @@ -25,8 +25,8 @@ class TestDirNotExistsError(UnitTestError, UserSideException): "A test folder '{0}' does not exist.\nPlease create 'test' " "directory in the project root and put a test set.\n" "More details about Unit " - "Testing: https://docs.platformio.org/page/plus/" - "unit-testing.html" + "Testing: https://docs.platformio.org/en/latest/advanced/" + "unit-testing/index.html" ) diff --git a/platformio/test/helpers.py b/platformio/test/helpers.py index a8e2f818..a789a6ea 100644 --- a/platformio/test/helpers.py +++ b/platformio/test/helpers.py @@ -24,7 +24,7 @@ def list_test_names(project_config): if not os.path.isdir(test_dir): raise TestDirNotExistsError(test_dir) names = [] - for root, _, __ in os.walk(test_dir): + for root, _, __ in os.walk(test_dir, followlinks=True): if not os.path.basename(root).startswith("test_"): continue names.append(os.path.relpath(root, test_dir).replace("\\", "/")) diff --git a/platformio/test/runners/base.py b/platformio/test/runners/base.py index ead621e5..2a0e6cec 100644 --- a/platformio/test/runners/base.py +++ b/platformio/test/runners/base.py @@ -28,7 +28,7 @@ CTX_META_TEST_RUNNING_NAME = __name__ + ".test_running_name" class TestRunnerOptions: # pylint: disable=too-many-instance-attributes def __init__( # pylint: disable=too-many-arguments self, - verbose=False, + verbose=0, without_building=False, without_uploading=False, without_testing=False, @@ -96,6 +96,8 @@ class TestRunnerBase: self.setup() for stage in ("building", "uploading", "testing"): getattr(self, f"stage_{stage}")() + if self.options.verbose: + click.echo() except Exception as exc: # pylint: disable=broad-except click.secho(str(exc), fg="red", err=True) self.test_suite.add_case( @@ -126,7 +128,7 @@ class TestRunnerBase: except ReturnErrorCode: raise UnitTestSuiteError( "Building stage has failed, see errors above. " - "Use `pio test --verbose` option to enable verbose output." + "Use `pio test -vvv` option to enable verbose output." ) def stage_uploading(self): @@ -145,7 +147,7 @@ class TestRunnerBase: except ReturnErrorCode: raise UnitTestSuiteError( "Uploading stage has failed, see errors above. " - "Use `pio test --verbose` option to enable verbose output." + "Use `pio test -vvv` option to enable verbose output." ) def stage_testing(self): @@ -172,15 +174,15 @@ class TestRunnerBase: def run_project_targets(self, targets): # pylint: disable=import-outside-toplevel - from platformio.commands.run.command import cli as run_cmd + from platformio.run.cli import cli as run_cmd assert self.cmd_ctx return self.cmd_ctx.invoke( run_cmd, project_conf=self.project_config.path, upload_port=self.options.upload_port, - verbose=self.options.verbose, - silent=not self.options.verbose, + verbose=self.options.verbose > 2, + silent=self.options.verbose < 2, environment=[self.test_suite.env_name], disable_auto_clean="nobuild" in targets, target=targets, diff --git a/platformio/test/runners/doctest.py b/platformio/test/runners/doctest.py index 15f6b311..d0fc931f 100644 --- a/platformio/test/runners/doctest.py +++ b/platformio/test/runners/doctest.py @@ -119,7 +119,7 @@ class DoctestTestRunner(TestRunnerBase): click.echo(line, nl=False) test_case = self._tc_parser.parse(line) - if test_case: + if test_case and not self.options.verbose: click.echo(test_case.humanize()) self.test_suite.add_case(test_case) diff --git a/platformio/test/runners/googletest.py b/platformio/test/runners/googletest.py index b3687bad..11e51ff6 100644 --- a/platformio/test/runners/googletest.py +++ b/platformio/test/runners/googletest.py @@ -22,7 +22,7 @@ from platformio.test.result import TestCase, TestCaseSource, TestStatus from platformio.test.runners.base import TestRunnerBase -class DoctestTestCaseParser: +class GoogletestTestCaseParser: # Examples: # [ RUN ] FooTest.Bar @@ -95,7 +95,7 @@ class GoogletestTestRunner(TestRunnerBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._tc_parser = DoctestTestCaseParser() + self._tc_parser = GoogletestTestCaseParser() os.environ["GTEST_COLOR"] = "no" # disable ANSI symbols def configure_build_env(self, env): @@ -110,7 +110,7 @@ class GoogletestTestRunner(TestRunnerBase): click.echo(line, nl=False) test_case = self._tc_parser.parse(line) - if test_case: + if test_case and not self.options.verbose: click.echo(test_case.humanize()) self.test_suite.add_case(test_case) diff --git a/platformio/test/runners/unity.py b/platformio/test/runners/unity.py index 6e0eae5a..e048c1ed 100644 --- a/platformio/test/runners/unity.py +++ b/platformio/test/runners/unity.py @@ -237,7 +237,8 @@ void unityOutputComplete(void) { unittest_uart_end(); } def generate_unity_extras(self, dst_dir): dst_dir = Path(dst_dir) - dst_dir.mkdir(parents=True, exist_ok=True) + if not dst_dir.is_dir(): + dst_dir.mkdir(parents=True) unity_h = dst_dir / "unity_config.h" if not unity_h.is_file(): unity_h.write_text( @@ -264,7 +265,7 @@ void unityOutputComplete(void) { unittest_uart_end(); } return test_case = self.parse_test_case(line) - if test_case: + if test_case and not self.options.verbose: click.echo(test_case.humanize()) if all(s in line for s in ("Tests", "Failures", "Ignored")): diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index b3dbaa57..00000000 --- a/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -filterwarnings = - error - # Marshmallow - ignore:distutils Version classes are deprecated. Use packaging.version instead. - ignore:The distutils package is deprecated and slated for removal in Python \ No newline at end of file diff --git a/scripts/99-platformio-udev.rules b/scripts/99-platformio-udev.rules index 6fd87d00..574b6fe6 100644 --- a/scripts/99-platformio-udev.rules +++ b/scripts/99-platformio-udev.rules @@ -16,7 +16,7 @@ # # INSTALLATION # -# Please visit > https://docs.platformio.org/en/latest/faq.html#platformio-udev-rules +# Please visit > https://docs.platformio.org/en/latest/core/installation/udev-rules.html # ##################################################################################### diff --git a/scripts/docspregen.py b/scripts/docspregen.py index 90dc887d..2846c7da 100644 --- a/scripts/docspregen.py +++ b/scripts/docspregen.py @@ -338,7 +338,7 @@ Packages .. warning:: **Linux Users**: - * Install "udev" rules :ref:`faq_udev_rules` + * Install "udev" rules :ref:`platformio_udev_rules` * Raspberry Pi users, please read this article `Enable serial port on Raspberry Pi `__. """ diff --git a/scripts/install_devplatforms.py b/scripts/install_devplatforms.py index 80526d90..20481ab4 100644 --- a/scripts/install_devplatforms.py +++ b/scripts/install_devplatforms.py @@ -22,25 +22,25 @@ import click @click.command() @click.option("--desktop", is_flag=True, default=False) @click.option( - "--ignore", - envvar="PIO_INSTALL_DEVPLATFORMS_IGNORE", - help="Ignore names split by comma", + "--names", + envvar="PIO_INSTALL_DEVPLATFORM_NAMES", + help="Install specified platform (split by comma)", ) @click.option( "--ownernames", - envvar="PIO_INSTALL_DEVPLATFORMS_OWNERNAMES", - help="Filter dev-platforms by ownernames (split by comma)", + envvar="PIO_INSTALL_DEVPLATFORM_OWNERNAMES", + help="Filter by ownernames (split by comma)", ) -def main(desktop, ignore, ownernames): +def main(desktop, names, ownernames): platforms = json.loads( subprocess.check_output(["pio", "platform", "search", "--json-output"]).decode() ) - ignore = [n.strip() for n in (ignore or "").split(",") if n.strip()] + names = [n.strip() for n in (names or "").split(",") if n.strip()] ownernames = [n.strip() for n in (ownernames or "").split(",") if n.strip()] for platform in platforms: skip = [ not desktop and platform["forDesktop"], - platform["name"] in ignore, + names and platform["name"] not in names, ownernames and platform["ownername"] not in ownernames, ] if any(skip): @@ -51,6 +51,7 @@ def main(desktop, ignore, ownernames): "pkg", "install", "--global", + "--skip-dependencies", "--platform", "{ownername}/{name}".format(**platform), ] diff --git a/setup.py b/setup.py index 5df72834..51853205 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ minimal_requirements = [ "pyelftools>=0.27,<1", "pyserial==3.*", "requests==2.*", - "semantic_version==2.9.*", + "semantic_version==2.10.*", "tabulate==0.8.*", "zeroconf<1", ] diff --git a/tests/commands/pkg/test_install.py b/tests/commands/pkg/test_install.py index d2aa25d6..3f3fcc96 100644 --- a/tests/commands/pkg/test_install.py +++ b/tests/commands/pkg/test_install.py @@ -395,7 +395,7 @@ def test_custom_project_libraries( ) assert pkgs_to_specs(lm.get_installed()) == [ PackageSpec("ArduinoJson@5.13.4"), - PackageSpec("Nanopb@0.4.6+3"), + PackageSpec("Nanopb@0.4.6+4"), ] assert config.get("env:devkit", "lib_deps") == [ "bblanchon/ArduinoJson@^5", diff --git a/tests/commands/pkg/test_outdated.py b/tests/commands/pkg/test_outdated.py index da1abda8..91180b42 100644 --- a/tests/commands/pkg/test_outdated.py +++ b/tests/commands/pkg/test_outdated.py @@ -51,7 +51,7 @@ def test_project(clirunner, validate_cliresult, isolated_pio_core, tmp_path): # validate output assert "Checking" in result.output assert re.search( - r"^atmelavr\s+2\.2\.0\s+3\.\d+\.\d+\s+3\.\d+\.\d+\s+Platform\s+devkit", + r"^atmelavr\s+2\.2\.0\s+3\.\d+\.\d+\s+[456789]\.\d+\.\d+\s+Platform\s+devkit", result.output, re.MULTILINE, ) diff --git a/tests/commands/test_account_org_team.py b/tests/commands/test_account_org_team.py index f04af536..4f23ff9b 100644 --- a/tests/commands/test_account_org_team.py +++ b/tests/commands/test_account_org_team.py @@ -21,47 +21,51 @@ import random import pytest import requests -from platformio.commands.account import cli as cmd_account -from platformio.commands.org import cli as cmd_org -from platformio.commands.team import cli as cmd_team +from platformio.account.cli import cli as cmd_account +from platformio.account.org.cli import cli as cmd_org +from platformio.account.team.cli import cli as cmd_team pytestmark = pytest.mark.skipif( - not (os.environ.get("TEST_EMAIL_LOGIN") and os.environ.get("TEST_EMAIL_PASSWORD")), - reason="requires TEST_EMAIL_LOGIN, TEST_EMAIL_PASSWORD environ variables", + not all( + os.environ.get(name) + for name in ( + "TEST_EMAIL_LOGIN", + "TEST_EMAIL_PASSWORD", + "TEST_EMAIL_IMAP_SERVER", + ) + ), + reason=( + "requires TEST_EMAIL_LOGIN, TEST_EMAIL_PASSWORD, " + "and TEST_EMAIL_IMAP_SERVER environment variables" + ), ) -username = None -email = None -splited_email = None -firstname = None -lastname = None -password = None +USER_NAME = "test-piocore-%s" % str(random.randint(0, 100000)) +USER_EMAIL = os.environ.get("TEST_EMAIL_LOGIN", "").replace("@", f"+{USER_NAME}@") +USER_PASSWORD = f"Qwerty-{random.randint(0, 100000)}" +USER_FIRST_NAME = "FirstName" +USER_LAST_NAME = "LastName" -orgname = None -display_name = None -second_username = None +ORG_NAME = "testorg-piocore-%s" % str(random.randint(0, 100000)) +ORG_DISPLAY_NAME = "Test Org for PIO Core" +EXISTING_OWNER = "piolabs" -teamname = None -team_description = None +TEAM_NAME = "test-" + str(random.randint(0, 100000)) +TEAM_DESCRIPTION = "team for CI test" -def test_prepare(): - global username, email, splited_email, firstname, lastname - global password, orgname, display_name, second_username, teamname, team_description - - username = "test-piocore-%s" % str(random.randint(0, 100000)) - splited_email = os.environ.get("TEST_EMAIL_LOGIN").split("@") - email = "%s+%s@%s" % (splited_email[0], username, splited_email[1]) - firstname = "Test" - lastname = "User" - password = "Qwerty123!" - - orgname = "testorg-piocore-%s" % str(random.randint(0, 100000)) - display_name = "Test Org for PIO Core" - second_username = "ivankravets" - - teamname = "test-" + str(random.randint(0, 100000)) - team_description = "team for CI test" +def verify_account(email_contents): + link = ( + email_contents.split("Click on the link below to start this process.")[1] + .split("This link will expire within 12 hours.")[0] + .strip() + ) + with requests.Session() as session: + result = session.get(link).text + link = result.split(' 0 + assert result.exit_code != 0 assert result.exception - assert "You are not authorized! Please log in to PIO Account" in str( - result.exception - ) + assert "You are not authorized!" in str(result.exception) + # use env tokem os.environ["PLATFORMIO_AUTH_TOKEN"] = token result = clirunner.invoke( cmd_account, - ["token", "--password", password, "--json-output"], + ["show", "--json-output"], ) validate_cliresult(result) json_result = json.loads(result.output.strip()) - assert json_result - assert json_result.get("status") == "success" - assert json_result.get("result") == token + assert json_result.get("user_id") + assert json_result.get("profile").get("username") == USER_NAME + assert json_result.get("profile").get("email") == USER_EMAIL os.environ.pop("PLATFORMIO_AUTH_TOKEN") result = clirunner.invoke( cmd_account, - ["login", "-u", username, "-p", password], + ["login", "-u", USER_NAME, "-p", USER_PASSWORD], ) validate_cliresult(result) @@ -234,7 +206,7 @@ def test_account_change_password(clirunner, validate_cliresult, isolated_pio_cor [ "password", "--old-password", - password, + USER_PASSWORD, "--new-password", new_password, ], @@ -242,11 +214,12 @@ def test_account_change_password(clirunner, validate_cliresult, isolated_pio_cor validate_cliresult(result) assert "Password successfully changed!" in result.output - clirunner.invoke(cmd_account, ["logout"]) + result = clirunner.invoke(cmd_account, ["logout"]) + validate_cliresult(result) result = clirunner.invoke( cmd_account, - ["login", "-u", username, "-p", new_password], + ["login", "-u", USER_NAME, "-p", new_password], ) validate_cliresult(result) @@ -257,7 +230,7 @@ def test_account_change_password(clirunner, validate_cliresult, isolated_pio_cor "--old-password", new_password, "--new-password", - password, + USER_PASSWORD, ], ) validate_cliresult(result) @@ -266,30 +239,29 @@ def test_account_change_password(clirunner, validate_cliresult, isolated_pio_cor def test_account_update( clirunner, validate_cliresult, receive_email, isolated_pio_core ): - global username - global email - global firstname - global lastname + global USER_NAME, USER_EMAIL, USER_FIRST_NAME, USER_LAST_NAME - firstname = "First " + str(random.randint(0, 100000)) - lastname = "Last" + str(random.randint(0, 100000)) + USER_NAME = "test-piocore-%s" % str(random.randint(0, 100000)) + USER_EMAIL = os.environ.get("TEST_EMAIL_LOGIN", "").replace( + "@", f"+new-{USER_NAME}@" + ) + USER_FIRST_NAME = "First " + str(random.randint(0, 100000)) + USER_LAST_NAME = "Last" + str(random.randint(0, 100000)) - username = "username" + str(random.randint(0, 100000)) - email = "%s+new-%s@%s" % (splited_email[0], username, splited_email[1]) result = clirunner.invoke( cmd_account, [ "update", "--current-password", - password, + USER_PASSWORD, "--firstname", - firstname, + USER_FIRST_NAME, "--lastname", - lastname, + USER_LAST_NAME, "--username", - username, + USER_NAME, "--email", - email, + USER_EMAIL, ], ) validate_cliresult(result) @@ -298,18 +270,7 @@ def test_account_update( "Please check your mail to verify your new email address and re-login. " in result.output ) - - result = receive_email(email) - link = ( - result.split("Click on the link below to start this process.")[1] - .split("This link will expire within 12 hours.")[0] - .strip() - ) - session = requests.Session() - result = session.get(link).text - link = result.split(' 0 assert result.exception - assert "You are not authorized! Please log in to PIO Account" in str( - result.exception - ) + assert "You are not authorized!" in str(result.exception) result = clirunner.invoke( cmd_account, - ["login", "-u", username, "-p", password], + ["login", "-u", USER_NAME, "-p", USER_PASSWORD], ) validate_cliresult(result) -# def test_account_destroy_with_linked_resources( +# def _test_account_destroy_with_linked_resources( # clirunner, validate_cliresult, receive_email, isolated_pio_core, tmpdir_factory # ): # package_url = "https://github.com/bblanchon/ArduinoJson/archive/v6.11.0.tar.gz" @@ -363,44 +322,47 @@ def test_account_update( def test_org_create(clirunner, validate_cliresult, isolated_pio_core): result = clirunner.invoke( cmd_org, - ["create", "--email", email, "--displayname", display_name, orgname], + ["create", "--email", USER_EMAIL, "--displayname", ORG_DISPLAY_NAME, ORG_NAME], ) validate_cliresult(result) def test_org_list(clirunner, validate_cliresult, isolated_pio_core): - # pio org list result = clirunner.invoke(cmd_org, ["list", "--json-output"]) validate_cliresult(result) json_result = json.loads(result.output.strip()) assert json_result == [ { - "orgname": orgname, - "displayname": display_name, - "email": email, + "orgname": ORG_NAME, + "displayname": ORG_DISPLAY_NAME, + "email": USER_EMAIL, "owners": [ - {"username": username, "firstname": firstname, "lastname": lastname} + { + "username": USER_NAME, + "firstname": USER_FIRST_NAME, + "lastname": USER_LAST_NAME, + } ], } ] def test_org_add_owner(clirunner, validate_cliresult, isolated_pio_core): - result = clirunner.invoke(cmd_org, ["add", orgname, second_username]) + result = clirunner.invoke(cmd_org, ["add", ORG_NAME, EXISTING_OWNER]) validate_cliresult(result) result = clirunner.invoke(cmd_org, ["list", "--json-output"]) validate_cliresult(result) - assert second_username in result.output + assert EXISTING_OWNER in result.output def test_org_remove_owner(clirunner, validate_cliresult, isolated_pio_core): - result = clirunner.invoke(cmd_org, ["remove", orgname, second_username]) + result = clirunner.invoke(cmd_org, ["remove", ORG_NAME, EXISTING_OWNER]) validate_cliresult(result) result = clirunner.invoke(cmd_org, ["list", "--json-output"]) validate_cliresult(result) - assert second_username not in result.output + assert EXISTING_OWNER not in result.output def test_org_update(clirunner, validate_cliresult, isolated_pio_core): @@ -411,8 +373,8 @@ def test_org_update(clirunner, validate_cliresult, isolated_pio_core): cmd_org, [ "update", - orgname, - "--new-orgname", + ORG_NAME, + "--orgname", new_orgname, "--displayname", new_display_name, @@ -427,9 +389,13 @@ def test_org_update(clirunner, validate_cliresult, isolated_pio_core): { "orgname": new_orgname, "displayname": new_display_name, - "email": email, + "email": USER_EMAIL, "owners": [ - {"username": username, "firstname": firstname, "lastname": lastname} + { + "username": USER_NAME, + "firstname": USER_FIRST_NAME, + "lastname": USER_LAST_NAME, + } ], } ] @@ -439,10 +405,10 @@ def test_org_update(clirunner, validate_cliresult, isolated_pio_core): [ "update", new_orgname, - "--new-orgname", - orgname, + "--orgname", + ORG_NAME, "--displayname", - display_name, + ORG_DISPLAY_NAME, ], ) validate_cliresult(result) @@ -453,9 +419,9 @@ def test_team_create(clirunner, validate_cliresult, isolated_pio_core): cmd_team, [ "create", - "%s:%s" % (orgname, teamname), + "%s:%s" % (ORG_NAME, TEAM_NAME), "--description", - team_description, + TEAM_DESCRIPTION, ], ) validate_cliresult(result) @@ -464,55 +430,55 @@ def test_team_create(clirunner, validate_cliresult, isolated_pio_core): def test_team_list(clirunner, validate_cliresult, isolated_pio_core): result = clirunner.invoke( cmd_team, - ["list", "%s" % orgname, "--json-output"], + ["list", "%s" % ORG_NAME, "--json-output"], ) validate_cliresult(result) json_result = json.loads(result.output.strip()) for item in json_result: del item["id"] assert json_result == [ - {"name": teamname, "description": team_description, "members": []} + {"name": TEAM_NAME, "description": TEAM_DESCRIPTION, "members": []} ] -def test_team_add_member(clirunner, validate_cliresult, isolated_pio_core): +def _test_team_add_member(clirunner, validate_cliresult, isolated_pio_core): result = clirunner.invoke( cmd_team, - ["add", "%s:%s" % (orgname, teamname), second_username], + ["add", "%s:%s" % (ORG_NAME, TEAM_NAME), EXISTING_OWNER], ) validate_cliresult(result) result = clirunner.invoke( cmd_team, - ["list", "%s" % orgname, "--json-output"], + ["list", "%s" % ORG_NAME, "--json-output"], ) validate_cliresult(result) - assert second_username in result.output + assert EXISTING_OWNER in result.output -def test_team_remove(clirunner, validate_cliresult, isolated_pio_core): +def _test_team_remove(clirunner, validate_cliresult, isolated_pio_core): result = clirunner.invoke( cmd_team, - ["remove", "%s:%s" % (orgname, teamname), second_username], + ["remove", "%s:%s" % (ORG_NAME, TEAM_NAME), EXISTING_OWNER], ) validate_cliresult(result) result = clirunner.invoke( cmd_team, - ["list", "%s" % orgname, "--json-output"], + ["list", "%s" % ORG_NAME, "--json-output"], ) validate_cliresult(result) - assert second_username not in result.output + assert EXISTING_OWNER not in result.output -def test_team_update(clirunner, validate_cliresult, receive_email, isolated_pio_core): +def _test_team_update(clirunner, validate_cliresult, receive_email, isolated_pio_core): new_teamname = "new-" + str(random.randint(0, 100000)) newteam_description = "Updated Description" result = clirunner.invoke( cmd_team, [ "update", - "%s:%s" % (orgname, teamname), + "%s:%s" % (ORG_NAME, TEAM_NAME), "--name", new_teamname, "--description", @@ -523,7 +489,7 @@ def test_team_update(clirunner, validate_cliresult, receive_email, isolated_pio_ result = clirunner.invoke( cmd_team, - ["list", "%s" % orgname, "--json-output"], + ["list", "%s" % ORG_NAME, "--json-output"], ) validate_cliresult(result) json_result = json.loads(result.output.strip()) @@ -537,20 +503,22 @@ def test_team_update(clirunner, validate_cliresult, receive_email, isolated_pio_ cmd_team, [ "update", - "%s:%s" % (orgname, new_teamname), + "%s:%s" % (ORG_NAME, new_teamname), "--name", - teamname, + TEAM_NAME, "--description", - team_description, + TEAM_DESCRIPTION, ], ) validate_cliresult(result) def test_cleanup(clirunner, validate_cliresult, receive_email, isolated_pio_core): - result = clirunner.invoke(cmd_team, ["destroy", "%s:%s" % (orgname, teamname)], "y") + result = clirunner.invoke( + cmd_team, ["destroy", "%s:%s" % (ORG_NAME, TEAM_NAME)], "y" + ) validate_cliresult(result) - result = clirunner.invoke(cmd_org, ["destroy", orgname], "y") + result = clirunner.invoke(cmd_org, ["destroy", ORG_NAME], "y") validate_cliresult(result) result = clirunner.invoke(cmd_account, ["destroy"], "y") validate_cliresult(result) diff --git a/tests/commands/test_check.py b/tests/commands/test_check.py index 746a81dd..be6042ca 100644 --- a/tests/commands/test_check.py +++ b/tests/commands/test_check.py @@ -21,7 +21,7 @@ from os.path import isfile, join import pytest from platformio import fs -from platformio.commands.check.command import cli as cmd_check +from platformio.check.cli import cli as cmd_check DEFAULT_CONFIG = """ [env:native] diff --git a/tests/commands/test_lib.py b/tests/commands/test_lib.py index 9429aaba..cf1e53b6 100644 --- a/tests/commands/test_lib.py +++ b/tests/commands/test_lib.py @@ -20,11 +20,11 @@ import os import pytest import semantic_version -from platformio.clients.registry import RegistryClient from platformio.commands.lib.command import cli as cmd_lib from platformio.package.meta import PackageType from platformio.package.vcsclient import VCSClientFactory from platformio.project.config import ProjectConfig +from platformio.registry.client import RegistryClient def test_saving_deps(clirunner, validate_cliresult, isolated_pio_core, tmpdir_factory): diff --git a/tests/commands/test_lib_complex.py b/tests/commands/test_lib_complex.py index ef0c0c4b..31f61fc6 100644 --- a/tests/commands/test_lib_complex.py +++ b/tests/commands/test_lib_complex.py @@ -17,7 +17,7 @@ import json import re -from platformio.commands import PlatformioCLI +from platformio.cli import PlatformioCLI from platformio.commands.lib.command import cli as cmd_lib from platformio.package.exception import UnknownPackageError from platformio.util import strip_ansi_codes diff --git a/tests/commands/test_run.py b/tests/commands/test_run.py index f5935668..5a2617fe 100644 --- a/tests/commands/test_run.py +++ b/tests/commands/test_run.py @@ -14,7 +14,7 @@ from pathlib import Path -from platformio.commands.run.command import cli as cmd_run +from platformio.run.cli import cli as cmd_run def test_build_flags(clirunner, validate_cliresult, tmpdir): @@ -29,6 +29,8 @@ def test_build_flags(clirunner, validate_cliresult, tmpdir): [env:native] platform = native extra_scripts = extra.py +lib_ldf_mode = deep+ +build_src_flags = -DI_AM_ONLY_SRC_FLAG build_flags = ; -DCOMMENTED_MACRO %s ; inline comment @@ -46,6 +48,12 @@ projenv.Append(CPPDEFINES="POST_SCRIPT_MACRO") tmpdir.mkdir("src").join("main.cpp").write( """ +#ifdef I_AM_ONLY_SRC_FLAG +#include +#else +#error "I_AM_ONLY_SRC_FLAG" +#endif + #if !defined(TEST_INT) || TEST_INT != 13 #error "TEST_INT" #endif @@ -54,6 +62,10 @@ projenv.Append(CPPDEFINES="POST_SCRIPT_MACRO") #error "TEST_STR_SPACE" #endif +#ifndef I_AM_COMPONENT +#error "I_AM_COMPONENT" +#endif + #ifndef POST_SCRIPT_MACRO #error "POST_SCRIPT_MACRO" #endif @@ -66,6 +78,27 @@ int main() { } """ ) + component_dir = tmpdir.mkdir("lib").mkdir("component") + component_dir.join("component.h").write( + """ +#define I_AM_COMPONENT + +#ifndef I_AM_ONLY_SRC_FLAG +#error "I_AM_ONLY_SRC_FLAG" +#endif + +void dummy(void); + """ + ) + component_dir.join("component.cpp").write( + """ +#ifdef I_AM_ONLY_SRC_FLAG +#error "I_AM_ONLY_SRC_FLAG" +#endif + +void dummy(void ) {}; + """ + ) result = clirunner.invoke(cmd_run, ["--project-dir", str(tmpdir), "--verbose"]) validate_cliresult(result) diff --git a/tests/commands/test_test.py b/tests/commands/test_test.py index db7acb13..2a6c7c56 100644 --- a/tests/commands/test_test.py +++ b/tests/commands/test_test.py @@ -21,7 +21,7 @@ import pytest from platformio import proc from platformio.fs import load_json -from platformio.test.command import test_cmd as pio_test_cmd +from platformio.test.cli import cli as pio_test_cmd def test_calculator_example(tmp_path: Path): @@ -187,6 +187,7 @@ int main() { ["-d", str(project_dir), "-e", "native", "--verbose"], ) validate_cliresult(result) + assert "1 Tests 0 Failures 0 Ignored" in result.output assert "Called from my_extra_fun" in result.output assert "CustomTestRunner::TearDown called" in result.output assert "Disabled test suite" not in result.output diff --git a/tests/conftest.py b/tests/conftest.py index 6f4a6088..b26aaf49 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ import time import pytest from click.testing import CliRunner -from platformio.clients import http +from platformio import http def pytest_configure(config): @@ -93,9 +93,9 @@ def without_internet(monkeypatch): @pytest.fixture def receive_email(): # pylint:disable=redefined-outer-name, too-many-locals def _receive_email(from_who): - test_email = os.environ.get("TEST_EMAIL_LOGIN") - test_password = os.environ.get("TEST_EMAIL_PASSWORD") - imap_server = os.environ.get("TEST_EMAIL_IMAP_SERVER") or "imap.gmail.com" + test_email = os.environ["TEST_EMAIL_LOGIN"] + test_password = os.environ["TEST_EMAIL_PASSWORD"] + imap_server = os.environ["TEST_EMAIL_IMAP_SERVER"] def get_body(msg): if msg.is_multipart(): diff --git a/tests/misc/__init__.py b/tests/misc/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/tests/misc/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/misc/ino2cpp/__init__.py b/tests/misc/ino2cpp/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/tests/misc/ino2cpp/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/ino2cpp/basic/basic.ino b/tests/misc/ino2cpp/examples/basic/basic.ino similarity index 100% rename from tests/ino2cpp/basic/basic.ino rename to tests/misc/ino2cpp/examples/basic/basic.ino diff --git a/tests/ino2cpp/multifiles/bar.ino b/tests/misc/ino2cpp/examples/multifiles/bar.ino similarity index 100% rename from tests/ino2cpp/multifiles/bar.ino rename to tests/misc/ino2cpp/examples/multifiles/bar.ino diff --git a/tests/ino2cpp/multifiles/foo.pde b/tests/misc/ino2cpp/examples/multifiles/foo.pde similarity index 100% rename from tests/ino2cpp/multifiles/foo.pde rename to tests/misc/ino2cpp/examples/multifiles/foo.pde diff --git a/tests/ino2cpp/strmultilines/main.ino b/tests/misc/ino2cpp/examples/strmultilines/main.ino similarity index 100% rename from tests/ino2cpp/strmultilines/main.ino rename to tests/misc/ino2cpp/examples/strmultilines/main.ino diff --git a/tests/test_ino2cpp.py b/tests/misc/ino2cpp/test_ino2cpp.py similarity index 79% rename from tests/test_ino2cpp.py rename to tests/misc/ino2cpp/test_ino2cpp.py index d1434df7..72e3bcdd 100644 --- a/tests/test_ino2cpp.py +++ b/tests/misc/ino2cpp/test_ino2cpp.py @@ -17,16 +17,16 @@ from os.path import dirname, isdir, join, normpath from platformio.commands.ci import cli as cmd_ci -INOTEST_DIR = normpath(join(dirname(__file__), "ino2cpp")) +EXAMPLES_DIR = normpath(join(dirname(__file__), "examples")) def pytest_generate_tests(metafunc): if "piotest_dir" not in metafunc.fixturenames: return test_dirs = [] - for name in listdir(INOTEST_DIR): - if isdir(join(INOTEST_DIR, name)): - test_dirs.append(join(INOTEST_DIR, name)) + for name in listdir(EXAMPLES_DIR): + if isdir(join(EXAMPLES_DIR, name)): + test_dirs.append(join(EXAMPLES_DIR, name)) test_dirs.sort() metafunc.parametrize("piotest_dir", test_dirs) @@ -37,10 +37,12 @@ def test_example(clirunner, validate_cliresult, piotest_dir): def test_warning_line(clirunner, validate_cliresult): - result = clirunner.invoke(cmd_ci, [join(INOTEST_DIR, "basic"), "-b", "uno"]) + result = clirunner.invoke(cmd_ci, [join(EXAMPLES_DIR, "basic"), "-b", "uno"]) validate_cliresult(result) assert 'basic.ino:16:14: warning: #warning "Line number is 16"' in result.output assert 'basic.ino:46:2: warning: #warning "Line number is 46"' in result.output - result = clirunner.invoke(cmd_ci, [join(INOTEST_DIR, "strmultilines"), "-b", "uno"]) + result = clirunner.invoke( + cmd_ci, [join(EXAMPLES_DIR, "strmultilines"), "-b", "uno"] + ) validate_cliresult(result) assert 'main.ino:75:2: warning: #warning "Line 75"' in result.output diff --git a/tests/test_maintenance.py b/tests/misc/test_maintenance.py similarity index 100% rename from tests/test_maintenance.py rename to tests/misc/test_maintenance.py diff --git a/tests/test_misc.py b/tests/misc/test_misc.py similarity index 91% rename from tests/test_misc.py rename to tests/misc/test_misc.py index 9b8fc5d4..52a6cac4 100644 --- a/tests/test_misc.py +++ b/tests/misc/test_misc.py @@ -17,9 +17,8 @@ import pytest import requests -from platformio import __check_internet_hosts__, proc -from platformio.clients import http -from platformio.clients.registry import RegistryClient +from platformio import __check_internet_hosts__, http, proc +from platformio.registry.client import RegistryClient def test_platformio_cli(): diff --git a/tox.ini b/tox.ini index 0ee9c0ec..92555cf5 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,12 @@ envlist = py36,py37,py38,py39 profile = black known_third_party=OpenSSL, SCons, jsonrpc, twisted, zope +[pytest] +filterwarnings = + error + # SCons + ignore:.*_ImportRedirect.find_spec() + [testenv] passenv = * usedevelop = True