[
  {
    "path": ".chglog/CHANGELOG.tpl.md",
    "content": "{{ if .Versions -}}\n<a name=\"unreleased\"></a>\n## [Unreleased]\n\n{{ if .Unreleased.CommitGroups -}}\n{{ range .Unreleased.CommitGroups -}}\n### {{ .Title }}\n{{ range .Commits -}}\n- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}\n{{ end }}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n\n{{ range .Versions }}\n<a name=\"{{ .Tag.Name }}\"></a>\n## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime \"2006-01-02\" .Tag.Date }}\n{{ range .CommitGroups -}}\n### {{ .Title }}\n{{ range .Commits -}}\n- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}\n{{ end }}\n{{ end -}}\n\n{{- if .RevertCommits -}}\n### Reverts\n{{ range .RevertCommits -}}\n- {{ .Revert.Header }}\n{{ end }}\n{{ end -}}\n\n{{- if .MergeCommits -}}\n### Pull Requests\n{{ range .MergeCommits -}}\n- {{ .Header }}\n{{ end }}\n{{ end -}}\n\n{{- if .NoteGroups -}}\n{{ range .NoteGroups -}}\n### {{ .Title }}\n{{ range .Notes }}\n{{ .Body }}\n{{ end }}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n\n{{- if .Versions }}\n[Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD\n{{ range .Versions -}}\n{{ if .Tag.Previous -}}\n[{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n"
  },
  {
    "path": ".chglog/config.yml",
    "content": "style: github\ntemplate: CHANGELOG.tpl.md\ninfo:\n  title: CHANGELOG\n  repository_url: https://github.com/BradenM/micropy-cli\noptions:\n  commits:\n    filters:\n      Type:\n        - feat\n        - fix\n        - perf\n        - refactor\n  commit_groups:\n    title_maps:\n      feat: Features\n      fix: Bug Fixes\n      perf: Performance Improvements\n      refactor: Code Refactoring\n  header:\n    pattern: \"^(\\\\w*)(?:\\\\(([\\\\w\\\\$\\\\.\\\\-\\\\*\\\\s]*)\\\\))?\\\\:\\\\s(.*)$\"\n    pattern_maps:\n      - Type\n      - Scope\n      - Subject\n\n  issues:\n    prefix:\n      -  #\n\n  refs:\n    actions:\n      - Closes\n      - Fixes\n\n  merges:\n    pattern: \"^Merge branch '(\\\\w+)'$\"\n    pattern_maps:\n      - Source\n\n  reverts:\n    pattern: \"^Revert \\\"([\\\\s\\\\S]*)\\\"$\"\n    pattern_maps:\n      - Header\n\n  notes:\n    keywords:\n      - BREAKING CHANGE\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\n\nroot = true\n\n[*]\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\ninsert_final_newline = true\ncharset = utf-8\nend_of_line = lf\n\n[LICENSE]\ninsert_final_newline = false\n\n[Makefile]\nindent_style = tab\n\n[*.py]\nprofile = black\n\n[*.y{,a}ml]\nindent_size = 2\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": "# Format all files with pre-commit->black, isort, autoflake and other hooks\n80755a39f3fbd9983a07ff4571197b8f6dcae1cb\n# style(pyupgrade): format all files\n402f8e68ee9c211e1f0238b8531b3ee0a9ce6806\n# style: format all with new pre-commit rules.\n0d679f27331c2ba851541c5d0ab34fe04923e660\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve Micropy Cli\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n<!--- Found a bug? Thanks for letting us know! -->\n\n<!--- Provide a general summary of the issue in the Title above -->\n\n**Describe the bug**\n<!--- Tell us what the bug is. -->\n\n**Expected Behavior**\n<!--- Tell us what you expect/should happen. -->\n\n**Current Behavior**\n<!--- Tell us what happens instead of the expected behavior -->\n\n**Steps to Reproduce**\n<!--- Provide a link to a live example, or an unambiguous set of steps to -->\n<!--- reproduce this bug. Include code to reproduce, if relevant -->\n\n1.\n2.\n3.\n\n\n**Possible Solution**\n<!--- Not obligatory, but suggest a fix/reason for the bug -->\n<!-- Feel free to delete if not applicable. -->\n\n\n**Logs**\n<!-- MicropyCli's log file can be found under $HOME/.micropy/micropy.log -->\n<!-- Please attach this file or provide the output relative to your issue -->\n<!-- using markdown ```<content>``` code tags. -->\n\n\n**Context (Environment)**\n<!-- Please provide the following information (if applicable) -->\n<!-- Explanations for each item can be found below -->\n\n<!-- OS? -->\n<!-- Windows/Linux/MacOS/etc, examples: Windows 10 or GNU/Linux 5.4.3-1-MANJARO -->\n\n<!-- Micropy Version? -->\n<!-- Can be found by running: micropy --version -->\n\n<!-- Python Version? -->\n<!-- Current Python version. -->\n\n<!-- VSCode Version? -->\n<!-- Current VSCode version. (If applicable)  -->\n\n* OS:\n* Micropy Version:\n* Python Version:\n* VSCode Version:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n    - name: Micropy Stubs\n      url: https://github.com/BradenM/micropy-stubs\n      about: Module missing or can't find your device? Make an issue on micropy-stubs.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n<!-- Got a feature you'd like to see implemented? Let us know! -->\n\n**Is your feature request related to a problem? Please describe.**\n<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->\n\n**Describe the solution you'd like**\n<!-- A clear and concise description of what you want to happen. -->\n\n**Describe alternatives you've considered**\n<!-- A clear and concise description of any alternative solutions or features you've considered. -->\n\n**Additional context**\n<!-- Add any other context or screenshots about the feature request here. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "content": "---\nname: Question\nabout: Got a question? Ask it!\ntitle: ''\nlabels: question\nassignees: ''\n\n---\n"
  },
  {
    "path": ".github/actions/setup-micropy/action.yml",
    "content": "name: \"Setup Micropy\"\ndescription: \"Setup micropy CI env.\"\ninputs:\n  poetry-version:\n    description: Poetry version to use.\n    required: true\n  poetry-install-url:\n    description: Poetry install url to use.\n    required: false\n    default: \"https://install.python-poetry.org\"\n  poetry-home:\n    description: Path to use as POETRY_HOME\n    required: false\n    default: \"/tmp/opt/poetry\"\n  python-version:\n    description: Python version to use.\n    required: true\n  runner:\n    description: Explicit runner cache to use.\n    required: false\n    default: \"\"\nruns:\n  using: composite\n  steps:\n    # See: https://github.com/actions/cache/blob/main/workarounds.md#improving-cache-restore-performance-on-windowsusing-cross-os-caching\n    - name: Use GNU tar\n      if: runner.os == 'Windows'\n      shell: cmd\n      run: |\n        echo \"Adding GNU tar to PATH\"\n        echo C:\\Program Files\\Git\\usr\\bin>>\"%GITHUB_PATH%\"\n\n    - name: Setup Poetry env.\n      shell: bash\n      run: |\n        echo 'POETRY_HOME=${{ inputs.poetry-home }}' >> $GITHUB_ENV\n        echo 'POETRY_VIRTUALENVS_IN_PROJECT=true' >> $GITHUB_ENV\n        echo 'POETRY_NO_INTERACTION=true' >> $GITHUB_ENV\n        echo 'POETRY_VERSION=${{ inputs.poetry-version }}' >> $GITHUB_ENV\n        echo \"${{ inputs.poetry-home }}/bin\" >> $GITHUB_PATH\n        echo \"$HOME/.local/bin\" >> $GITHUB_PATH\n\n    - name: Setup Win Path\n      if: runner.os == 'Windows'\n      shell: pwsh\n      run: |\n        echo \"C:\\Users\\runneradmin\\AppData\\Local\\Temp\\opt\\poetry\\bin\" | Out-File -FilePath $env:GITHUB_PATH -Append\n        echo \"C:\\Users\\runneradmin\\AppData\\Local\\Temp\\opt\\poetry\\venv\\Scripts\" | Out-File -FilePath $env:GITHUB_PATH -Append\n\n    - name: Workaround Poetry v1.4.0 Windows issues.\n      if: runner.os == 'Windows'\n      shell: bash\n      run: |\n        # have not looked into why this occurs.\n        # just disable new installer for windows.\n        echo 'POETRY_INSTALLER_MODERN_INSTALLATION=false' >> $GITHUB_ENV\n\n    - name: Cache poetry install.\n      uses: actions/cache@v3\n      id: poetry-install-cache\n      with:\n        path: ${{ inputs.poetry-home }}/install-poetry.py\n        key: poetry-install-${{ inputs.runner || matrix.os || runner.os }}-${{ inputs.poetry-version }}\n\n    - name: Fetch Poetry Installer\n      shell: bash\n      if: steps.poetry-install-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p \"${{ inputs.poetry-home }}\"\n        curl -sSL -o ${{ inputs.poetry-home }}/install-poetry.py ${{ inputs.poetry-install-url }}\n\n    - name: Set up Python ${{ inputs.python-version }}\n      uses: actions/setup-python@v4\n      id: python-setup\n      with:\n        python-version: ${{ inputs.python-version }}\n\n    - name: Install Poetry\n      shell: bash\n      run: |\n        python ${{ inputs.poetry-home }}/install-poetry.py --version ${{ inputs.poetry-version }}\n        ${{ inputs.poetry-home }}/bin/poetry --version\n\n    - name: Get poetry cache dir.\n      id: poetry-config\n      shell: bash\n      run: |\n        POETRY=\"${{ inputs.poetry-home }}/bin/poetry\"\n        CACHE_DIR=\"$($POETRY config cache-dir)\"\n        echo \"Poetry cache: $CACHE_DIR\"\n        echo \"cache-dir=$CACHE_DIR\" >> $GITHUB_OUTPUT\n\n    - name: Cache poetry cache.\n      uses: actions/cache@v3\n      with:\n        path: ${{ steps.poetry-config.outputs.cache-dir }}\n        key: poetry-cache-${{ inputs.runner || matrix.os || runner.os }}-${{ inputs.python-version }}-${{ inputs.poetry-version }}\n\n    - name: Cache virtual env.\n      uses: actions/cache@v3\n      id: venv-cache\n      with:\n        path: .venv\n        key: poetry-venv-${{ inputs.runner || matrix.os || runner.os }}-${{ inputs.python-version }}-${{ inputs.poetry-version }}-${{ hashFiles('poetry.lock') }}\n        restore-keys: |\n          poetry-venv-${{ inputs.runner || matrix.os || runner.os }}-${{ inputs.python-version }}-${{ inputs.poetry-version }}-\n\n    - name: Install dependencies.\n      shell: bash\n      if: steps.venv-cache.outputs.cache-hit != 'true'\n      run: |\n        ${{ inputs.poetry-home }}/bin/poetry install --with docs --with test -v\n        ${{ inputs.poetry-home }}/bin/poetry env info\n"
  },
  {
    "path": ".github/codeql/codeql-config.yml",
    "content": "# do not scan entire .venv\n# security alters stemming from dependencies are handled elsewhere (dependabot alerts+github).\npaths-ignore:\n  - .venv\n  - test\n"
  },
  {
    "path": ".github/renovate.json5",
    "content": "{\n    $schema: \"https://docs.renovatebot.com/renovate-schema.json\",\n    extends: [\n        \"config:base\",\n        \":rebaseStalePrs\",\n        \":prConcurrentLimit20\",\n        \":prHourlyLimitNone\",\n        \":pinDependencies\",\n        \":automergeMinor\",\n        \":automergeDigest\",\n    ],\n    addLabels: [\"dependencies\"],\n    major: {\n        automerge: false,\n    },\n    ignorePaths: [\"docs/requirements.txt\"],\n    dependencyDashboard: true,\n    packageRules: [\n        {\n            matchDepTypes: [\"devDependencies\"],\n            matchUpdateTypes: [\"minor\", \"patch\"],\n            automerge: true,\n            groupName: \"devDependencies (non-major)\"\n        },\n        {\n            matchDepTypes: [\"devDependencies\"],\n            matchUpdateTypes:  [\"major\"],\n            automerge: true\n        }\n    ],\n}\n"
  },
  {
    "path": ".github/workflows/changelog.yml",
    "content": "name: Changelog\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  changelog:\n    name: Generate Changelog\n    runs-on: ubuntu-latest\n    continue-on-error: true\n    if: github.actor != 'github-actions[bot]' || github.event.pusher.email != 'github-actions[bot]@users.noreply.github.com'\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          token: ${{ secrets.GH_PAT }}\n          fetch-depth: 0\n\n      - name: Setup go\n        uses: actions/setup-go@v3\n\n      - name: Generate Changelog\n        run: go run github.com/git-chglog/git-chglog/cmd/git-chglog@latest --output CHANGELOG.md\n\n      - name: Commit Changes\n        run: |\n          git config --local user.email \"github-actions[bot]@users.noreply.github.com\"\n          git config --local user.name \"github-actions[bot]\"\n          git commit -m \"chore(chglog): update changelog.\" -a || true\n\n      - name: Push Changes\n        uses: ad-m/github-push-action@77c5b412c50b723d2a4fbc6d71fb5723bcd439aa\n        with:\n          github_token: ${{ secrets.GH_PAT }}\n          branch: ${{ github.ref }}\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [master]\n  schedule:\n    - cron: \"24 9 * * 4\"\n\nenv:\n  POETRY_VERSION: 1.8.3\n  PYTHON_VERSION: 3.11\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [\"python\"]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Learn more about CodeQL language support at https://git.io/codeql-language-support\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup environment.\n        uses: ./.github/actions/setup-micropy\n        with:\n          poetry-version: ${{ env.POETRY_VERSION }}\n          python-version: ${{ env.PYTHON_VERSION }}\n          runner: ubuntu-latest\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v2\n        env:\n          CODEQL_PYTHON: ./.venv/bin/python\n        with:\n          languages: ${{ matrix.language }}\n          setup-python-dependencies: false\n          config-file: ./.github/codeql/codeql-config.yml\n          source-root: micropy\n          # If you wish to specify custom queries, you can do so here or in a config file.\n          # By default, queries listed here will override any specified in a config file.\n          # Prefix the list here with \"+\" to use these queries and those in the config file.\n          # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n      - name: Perform CodeQL Analysis\n        env:\n          CODEQL_PYTHON: ./.venv/bin/python\n        uses: github/codeql-action/analyze@v2\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Test MicropyCli\n\non:\n  pull_request: ~\n  push:\n    branches:\n      - master\n\nenv:\n  POETRY_VERSION: 1.8.3\n\nconcurrency:\n  group: main-${{ github.event_name }}-${{ github.ref }}\n  cancel-in-progress: true\n\ndefaults:\n  run:\n    shell: bash\n\njobs:\n  test:\n    name: ${{ matrix.os }} @ Py v${{ matrix.python }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        # explicitly use macOS-12 to avoid\n        # macOS-11 cached wheels failing to install on a 12 runner.\n        # this is due to an active transition by github.\n        # see: https://github.blog/changelog/2022-10-03-github-actions-jobs-running-on-macos-latest-are-now-running-on-macos-12/\n        os: [windows-latest, macOS-12, ubuntu-latest]\n        python: [\"3.9\", \"3.10\", \"3.11\"]\n\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          submodules: true\n\n      - name: Setup environment.\n        uses: ./.github/actions/setup-micropy\n        with:\n          poetry-version: ${{ env.POETRY_VERSION }}\n          python-version: ${{ matrix.python }}\n\n      - name: Run Tests\n        run: poetry run pytest --cov --cov-config=pyproject.toml --junit-xml=test_log.xml --cov-report=xml:cov.xml -vv -ra -n'auto'\n\n      - name: Upload Codecov\n        uses: codecov/codecov-action@v3\n        env:\n          OS: ${{ matrix.os }}\n          PYTHON: ${{ matrix.python }}\n        with:\n          files: ./cov.xml\n          fail_ci_if_error: false\n          flags: unittests,py-${{ matrix.python }},os-${{ matrix.os }}\n          env_vars: OS,PYTHON\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish Release\n\non:\n  release:\n    types:\n      - created\n\nenv:\n  POETRY_VERSION: 1.8.3\n  PYTHON_VERSION: 3.11\n\njobs:\n  publish:\n    name: Publish\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          submodules: true\n\n      - name: Setup environment.\n        uses: ./.github/actions/setup-micropy\n        with:\n          poetry-version: ${{ env.POETRY_VERSION }}\n          python-version: ${{ env.PYTHON_VERSION }}\n\n      - name: Build\n        run: poetry build\n\n      - name: Publish to PyPi\n        env:\n          POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}\n        run: poetry publish\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - master\n\njobs:\n  release-please:\n    name: Release Please\n    runs-on: ubuntu-latest\n    steps:\n      - name: Release Please\n        id: release-please\n        # see: googleapis/release-please#1837\n        uses: BradenM/release-please-action@d0fa220390843191f01153795b2e5dce67410563\n        with:\n          token: ${{ secrets.GH_PAT }}\n          command: manifest\n"
  },
  {
    "path": ".gitignore",
    "content": "\n# Created by https://www.gitignore.io/api/python\n# Edit at https://www.gitignore.io/?templates=python\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\ncov.xml\ntest_log.xml\n.testmondata\n.tmontmp/\n\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don’t work, or not\n#   install all needed dependencies.\nPipfile.lock\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n.envrc\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# End of https://www.gitignore.io/api/python\n\n.vscode\n!.vscode/tasks.json\n!micropy/project/template/**/*\n!micropy/lib\n!tests/data/project_test/**/*\n\n# Created by https://www.toptal.com/developers/gitignore/api/pycharm+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all\n\n### PyCharm+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### PyCharm+all Patch ###\n# Ignores the whole .idea folder and all .iml files\n# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360\n\n.idea/\n\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# Sonarlint plugin\n.idea/sonarlint\n\n# End of https://www.toptal.com/developers/gitignore/api/pycharm+all\n\ntemp/*\n\n# Created by https://www.toptal.com/developers/gitignore/api/direnv\n# Edit at https://www.toptal.com/developers/gitignore?templates=direnv\n\n### direnv ###\n.direnv\n.envrc\n\n# End of https://www.toptal.com/developers/gitignore/api/direnv\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\nexclude: |\n  (?x)^(\n    /(\n        \\.eggs\n      | \\.git\n      | \\.hg\n      | \\.mypy_cache\n      | \\.tox\n      | \\.venv\n      | _build\n      | build\n      | dist\n      | micropy/lib\n    )/\n    | foo.py\n  )$\n\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.4.0\n    hooks:\n      - id: debug-statements\n      - id: detect-private-key\n      - id: end-of-file-fixer\n      - id: check-executables-have-shebangs\n  - repo: https://github.com/charliermarsh/ruff-pre-commit\n    rev: \"v0.0.255\"\n    hooks:\n      - id: ruff\n        args:\n          - --fix\n  - repo: https://github.com/pycqa/isort\n    rev: 5.12.0\n    hooks:\n      - id: isort\n  - repo: https://github.com/psf/black\n    rev: 23.1.0\n    hooks:\n      - id: black\n  - repo: https://github.com/python-poetry/poetry\n    rev: 1.4.1\n    hooks:\n      - id: poetry-check\n        files: \"^(pyproject.toml|poetry.lock)$\"\n\nci:\n  autofix_commit_msg: \"ci(pre-commit.ci): 🎨 Auto format from pre-commit.com hooks\"\n  autoupdate_commit_msg: \"ci(pre-commit.ci): ⬆ pre-commit autoupdate\"\n"
  },
  {
    "path": ".prettierignore",
    "content": "micropy/template/**\n"
  },
  {
    "path": ".readthedocs.yml",
    "content": "# .readthedocs.yml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Build documentation in the docs/ directory with Sphinx\nsphinx:\n  configuration: docs/conf.py\n\n# Fetch Submodules\nsubmodules:\n  include: all\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats: all\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n  jobs:\n    # see: https://docs.readthedocs.io/en/stable/build-customization.html#install-dependencies-with-poetry\n    post_create_environment:\n      - pip install poetry==1.4.1\n      - poetry config virtualenvs.create false\n    post_install:\n      - poetry install --with docs\n"
  },
  {
    "path": ".release-please-manifest.json",
    "content": "{\n    \".\": \"4.2.2\"\n}\n"
  },
  {
    "path": ".tool-versions",
    "content": "python     3.11.5\npoetry     1.8.3\ngit-chglog 0.15.2\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "<a name=\"v4.0.0\"></a>\n\n## [4.2.2](https://github.com/BradenM/micropy-cli/compare/v4.2.1...v4.2.2) (2023-06-14)\n\n\n### Bug Fixes\n\n* **cli:** Re-add version as command ([6632c5b](https://github.com/BradenM/micropy-cli/commit/6632c5bbe82f5ebf6f306c5fbe500213d208a0fb))\n* **deps:** Update dependency libcst to v0.4.10 ([5b33886](https://github.com/BradenM/micropy-cli/commit/5b33886eb6210231a7afc4be5920f7b7c92fd762))\n* **deps:** Update dependency markupsafe to v2.1.3 ([2b5e7a3](https://github.com/BradenM/micropy-cli/commit/2b5e7a38f53cc7605a310187817a9a85a11b7624))\n* **deps:** Update dependency pydantic to v1.10.8 ([3672fcf](https://github.com/BradenM/micropy-cli/commit/3672fcf6997647366abea9672c102dc24bc5bf77))\n* **deps:** Update dependency pydantic to v1.10.9 ([66fa6e5](https://github.com/BradenM/micropy-cli/commit/66fa6e5147b29183c09dadfd4dd65e7956111596))\n* **deps:** Update dependency requests to v2.31.0 ([352d3b3](https://github.com/BradenM/micropy-cli/commit/352d3b3ad09c0723c7b9572245c336ccac22afb7))\n* **deps:** Update dependency typing-extensions to v4.6.0 ([4858007](https://github.com/BradenM/micropy-cli/commit/4858007c327a8a258196ecbca4fe4507da90332b))\n* **deps:** Update dependency typing-extensions to v4.6.1 ([24c16f2](https://github.com/BradenM/micropy-cli/commit/24c16f2978f08a6e2ac4387354ed7e309ff2158c))\n* **deps:** Update dependency typing-extensions to v4.6.2 ([98ae691](https://github.com/BradenM/micropy-cli/commit/98ae691da95de484e9dd68e0a13f79400755168c))\n* **deps:** Update dependency typing-extensions to v4.6.3 ([dd993e5](https://github.com/BradenM/micropy-cli/commit/dd993e517bab238d01aa11d03bb79aac2fe50020))\n\n## [4.2.1](https://github.com/BradenM/micropy-cli/compare/v4.2.0...v4.2.1) (2023-05-18)\n\n\n### Bug Fixes\n\n* **deps:** Update dependency attrs to v23 ([3422a84](https://github.com/BradenM/micropy-cli/commit/3422a84f576e2db7e39331512e71e3d72ed85d55))\n* **deps:** Update dependency python-minifier to v2.9.0 ([2a7464c](https://github.com/BradenM/micropy-cli/commit/2a7464cf9be34dc4766083fab6570fe0c507e5d7))\n* **deps:** Update dependency requests to v2.29.0 ([8361e7c](https://github.com/BradenM/micropy-cli/commit/8361e7cc15b0bdcefa07048814f30c0d4bb531b3))\n* **deps:** Update dependency requests to v2.30.0 ([9e389b1](https://github.com/BradenM/micropy-cli/commit/9e389b11c171d16af26b28217993e38bbbf4fa4f))\n* **deps:** Update dependency typer to v0.8.0 ([d2bf4e6](https://github.com/BradenM/micropy-cli/commit/d2bf4e69e83da62b589658ed3210742152547db6))\n* **deps:** Update dependency typer to v0.9.0 ([f8f4105](https://github.com/BradenM/micropy-cli/commit/f8f4105e65370eb275114f1e30ce5c3361e61dd5))\n* **stubs:** Always ensure correct pyi stub root paths. ([27f87a1](https://github.com/BradenM/micropy-cli/commit/27f87a1d697e2ee5e18d178eb46c7532b3c2fe29))\n* **stubs:** Do not drop firmware name when parsing from dist metadata. ([be269ee](https://github.com/BradenM/micropy-cli/commit/be269ee5e241d9274d100c5dc1c9f28278b0d1d6))\n* **template:** Resolve pylance type-checking / import errors. ([179c29d](https://github.com/BradenM/micropy-cli/commit/179c29d68bf025c1b3436d53dd3f9168d93285cf))\n* **template:** Update pylint config to use `MAIN` and `INFERENCE` confidence level. ([f5f5c98](https://github.com/BradenM/micropy-cli/commit/f5f5c983cbeeb6730c85750ac78115d86135d44b))\n\n## [4.2.0](https://github.com/BradenM/micropy-cli/compare/v4.2.0-beta.3...v4.2.0) (2023-04-22)\n\n\n### Features\n\n* **app:** Add exclude_defaults, improve help in stubs create command. ([efa3263](https://github.com/BradenM/micropy-cli/commit/efa3263121704e56d6e4dc0aa4975228b4731184))\n\n\n### Documentation\n\n* **app:** Improve stubs create help. ([9738ac7](https://github.com/BradenM/micropy-cli/commit/9738ac71256e8375ee190f3114ef5cb7e46c3506))\n\n\n### Miscellaneous Chores\n\n* **release:** Release as 4.2.0 ([817c583](https://github.com/BradenM/micropy-cli/commit/817c583bc460e642ddad0b383d88483945ee234b))\n\n## [4.2.0-beta.3](https://github.com/BradenM/micropy-cli/compare/v4.2.0-beta.2...v4.2.0-beta.3) (2023-04-17)\n\n\n### Features\n\n* **app:** Expose compile, module-defaults flags, integrity in stubs create command. ([885e9d3](https://github.com/BradenM/micropy-cli/commit/885e9d369cad98345e6132315f6fb76694379339))\n* **exc:** `PyDeviceFileIntegrityError` exception. ([a2187c8](https://github.com/BradenM/micropy-cli/commit/a2187c898c9aeb641a63a9237d2469faaf929de5))\n* **pyd:** `NoOpConsumer` implementation. ([5a21ecd](https://github.com/BradenM/micropy-cli/commit/5a21ecd1e85f74a5976aac209457d8273d8596cb))\n* **pydevice:** File integrity support, simple run in pydevice. ([8b7a255](https://github.com/BradenM/micropy-cli/commit/8b7a2557d42cb70430430ad04280dffebd45aeb6))\n* **pyd:** File integrity check support in upydevice backend. ([a3cbf31](https://github.com/BradenM/micropy-cli/commit/a3cbf31f8ff5fd5f0da24661a911f030672d93b7))\n* **pyd:** Implement `remove` across backends. ([f0bba8d](https://github.com/BradenM/micropy-cli/commit/f0bba8d25cd7eb6bdb2567bfde97c22a02088c16))\n* **pyd:** Use noop consumer default for upy `eval`/`eval_script` ([720ace4](https://github.com/BradenM/micropy-cli/commit/720ace4355d5e7a794e6d204a6bccfe0249d5f5d))\n* **utils:** Support compiling createstubs with mpy-cross ([1819197](https://github.com/BradenM/micropy-cli/commit/181919714c6d5d9a5a382e673bd81334164cbb53))\n\n\n### Bug Fixes\n\n* **deps:** Pin dependency typer to 0.7.0 ([30ab97f](https://github.com/BradenM/micropy-cli/commit/30ab97fc450ae5af9f58cf3e5770ce609cba9745))\n* **pyd:** Remove usedforsecurity hash flag for py3.8 ([522a0c3](https://github.com/BradenM/micropy-cli/commit/522a0c3537634950178178628d097944ce554cb3))\n* **pyd:** Support pushing binary files to pydevice in upydevice backend. ([e58c1f2](https://github.com/BradenM/micropy-cli/commit/e58c1f2f90f6e901298dfe54d0b5234818107140))\n\n\n### Miscellaneous Chores\n\n* **release:** Set release to v4.2.0-beta3 ([9ee81a2](https://github.com/BradenM/micropy-cli/commit/9ee81a2f4f63de78716b73b2bc4224a50e58a2c4))\n\n## [4.2.0-beta.2](https://github.com/BradenM/micropy-cli/compare/v4.2.0-beta.1...v4.2.0-beta.2) (2023-03-27)\n\n\n### Bug Fixes\n\n* **deps:** Use latest stable typer. ([ec3082e](https://github.com/BradenM/micropy-cli/commit/ec3082ed541b724743f8bc7b5bb19ecce9c9e2bc))\n\n## [4.2.0-beta.1](https://github.com/BradenM/micropy-cli/compare/v4.2.0-beta...v4.2.0-beta.1) (2023-03-27)\n\n\n### Features\n\n* **app:** Extract main cli logic out to command/option callbacks. ([e3f6401](https://github.com/BradenM/micropy-cli/commit/e3f6401f99faebaec13946b371b966d22edd681b)), closes [#338](https://github.com/BradenM/micropy-cli/issues/338)\n* **app:** Implement main app cli entries with typer. ([d2b22a5](https://github.com/BradenM/micropy-cli/commit/d2b22a567a0e0e084378f0279aa23f5802fa8bc6))\n* **app:** Implement stubs subcommand with typer. ([7733f24](https://github.com/BradenM/micropy-cli/commit/7733f243c62065500be46b5995819076c41fae32))\n* **app:** Link to Josverl/micropython-stubber in create help ([88fb8dd](https://github.com/BradenM/micropy-cli/commit/88fb8dd8a90d120483ae008e1d46e553293032c6))\n* **cli:** Remove old cli module. ([8e14fa6](https://github.com/BradenM/micropy-cli/commit/8e14fa691158a4c537cd89d39db1b92cd208bc42))\n* **deps:** Add rich, typer, shellingham ([62f7c72](https://github.com/BradenM/micropy-cli/commit/62f7c729442399038b326628fa5c96c26b0e84a3))\n* **deps:** Remove pytest-clarity ([d712580](https://github.com/BradenM/micropy-cli/commit/d71258076ed85343a5a145418e5dab11f446b85e))\n* **dev:** Add and utilize external mock with pytest-mock ([79b3d96](https://github.com/BradenM/micropy-cli/commit/79b3d965f46a4ffd1e9bb972267f80ae77b68fda))\n* **dev:** Add pdbpp to dev deps. ([239cb3e](https://github.com/BradenM/micropy-cli/commit/239cb3ebc690fd83583d4126cecae7268aea7c4e))\n* **main:** Allow override of primary data directories. ([1689bfd](https://github.com/BradenM/micropy-cli/commit/1689bfdca7c8dcdb3b4cf137978c51a2ee33506e))\n* **main:** Remove stub creation logic from micropy main state. ([1f8d9ba](https://github.com/BradenM/micropy-cli/commit/1f8d9baf32e742757f8bef348e90b289317cf9b4))\n* **utils:** Remove import catch for stubber. ([91103e6](https://github.com/BradenM/micropy-cli/commit/91103e6e790954648ccf6d7f50d5049bfc477bfa))\n\n\n### Bug Fixes\n\n* **app:** Use future annotations. ([450c27d](https://github.com/BradenM/micropy-cli/commit/450c27d9f1b6748464715e888144eca9d0ac2c62))\n* **compat:** Typer list type errors on py3.8 ([3483302](https://github.com/BradenM/micropy-cli/commit/34833023e9c80e74b02cea1d51447a6b203064b7))\n* **compat:** Use typing.Type in app.stubs ([00858c2](https://github.com/BradenM/micropy-cli/commit/00858c292d4a098b4414138842a97761384cbaef))\n* **deps:** Exclude pdbpp on windows ([445455e](https://github.com/BradenM/micropy-cli/commit/445455e07917cc1b7f647bc7cb89a65050a2ade9))\n* **deps:** Update dependency pydantic to v1.10.7 ([36e4727](https://github.com/BradenM/micropy-cli/commit/36e47271a1da50a9c1a35646b0f82011781bf721))\n\n\n### Miscellaneous Chores\n\n* **release:** Set release to v4.2.0-beta.1 ([614d3aa](https://github.com/BradenM/micropy-cli/commit/614d3aac359373d929b113874d7d73976a2cbb3f))\n\n## [4.2.0-beta](https://github.com/BradenM/micropy-cli/compare/v4.1.0...v4.2.0-beta) (2023-03-20)\n\n\n### Features\n\n* **cli:** Expose backend option to select pydevice backend. ([43f3751](https://github.com/BradenM/micropy-cli/commit/43f3751620acd6fcb49d0e4cc63d9afd4b998857))\n* **deps:** Add libcst as dependency, remove py38 constraint from ([9bdb811](https://github.com/BradenM/micropy-cli/commit/9bdb811a2be6c2258f9e3619b9a9265565fe5ab7))\n* **deps:** Add lint dependency group, remove unused/replaced with ([f820baa](https://github.com/BradenM/micropy-cli/commit/f820baae961b41d3386f1823565709be67436df0))\n* **deps:** Add micropython-stubber as proper library. ([d82f5fc](https://github.com/BradenM/micropy-cli/commit/d82f5fcc49f3adc3c25a1eea010091f48ebbe056))\n* **dx:** Replace pyupgrade/autoflake hooks with ruff ([3c47ebd](https://github.com/BradenM/micropy-cli/commit/3c47ebdee44547c2f3370fb18f6b6ef0ba5b7617))\n* **lib:** Remove old micropython-stubber submodule. ([ea1ee8c](https://github.com/BradenM/micropy-cli/commit/ea1ee8cdebc8ed2e7be593412e069fd1c1a2fd39))\n* **main:** Support create stubs backend parameter, utilize create stub variant. ([5cb26c3](https://github.com/BradenM/micropy-cli/commit/5cb26c38e06606b84f515ac7f8f0506ddf057e8b))\n* **pkg:** Drop support for python 3.7 ([87eb790](https://github.com/BradenM/micropy-cli/commit/87eb7901fe7d0ba5cd974629fadbb82524603af7))\n* **utils:** Prepare create stubs with codemod variants/modules, update stubmaker imports. ([2b8de82](https://github.com/BradenM/micropy-cli/commit/2b8de8298d5220f301b792f0718e74be5429904c))\n\n\n### Bug Fixes\n\n* **deps:** Pin dependencies ([9a7f407](https://github.com/BradenM/micropy-cli/commit/9a7f407e39be33a639c48de3527d6482ece82f26))\n* **deps:** Remove pypi-test sourced from pyproject ([b3fc9e3](https://github.com/BradenM/micropy-cli/commit/b3fc9e3b09002c5d5c17fe28173055cc3d3d830e))\n* **deps:** Target isort &lt;5.12.0 when on py3.7 ([f936752](https://github.com/BradenM/micropy-cli/commit/f936752a73898e9dce3ef5f3f4763384db0eab79))\n* **deps:** Target pylint &lt;2.13 when on py3.7 ([80dc833](https://github.com/BradenM/micropy-cli/commit/80dc833bb6128c033e1278ba5f3a93afe70ff2e8))\n* **deps:** Update dependency boltons to v23 ([27238ba](https://github.com/BradenM/micropy-cli/commit/27238ba47d817e3286b4a1b4fa0ccc1155985e6a))\n* **deps:** Update dependency mypy to v1.1.1 ([a88668c](https://github.com/BradenM/micropy-cli/commit/a88668c39019ee466a388c0ca868305687537002))\n* **deps:** Update dependency pydantic to v1.10.6 ([8c600f2](https://github.com/BradenM/micropy-cli/commit/8c600f24f03ae2b1349b9c483f46c21a76de3e51))\n* **deps:** Update dependency python-minifier to v2.8.1 ([24ce47d](https://github.com/BradenM/micropy-cli/commit/24ce47d875e9bc5a0c4f20bfd582a313642aac78))\n\n\n### Documentation\n\n* **cfg:** Remove requirements ([2b54413](https://github.com/BradenM/micropy-cli/commit/2b54413c2e4923aeb4ed7da2e3b78d92407b3db0))\n* **cfg:** Update rtd config to setup env w/ poetry. ([82c4da0](https://github.com/BradenM/micropy-cli/commit/82c4da02955080e5202bb1630a69daadce8323f4))\n\n\n### Miscellaneous Chores\n\n* **release:** Set release v4.2.0-beta ([0e2d138](https://github.com/BradenM/micropy-cli/commit/0e2d13883f7c45c36a75ee3d51e6dd63057b0b96))\n\n## [4.1.0](https://github.com/BradenM/micropy-cli/compare/v4.1.0-beta...v4.1.0) (2023-03-05)\n\n\n### Bug Fixes\n\n* **deps:** Update dependency cachier to v2 ([956cce8](https://github.com/BradenM/micropy-cli/commit/956cce8ca8c594659fd3e57a77c65d0d65036ff9))\n* **deps:** Update dependency gitpython to v3.1.31 ([65b3e83](https://github.com/BradenM/micropy-cli/commit/65b3e83fa7136765c10f268409d002f3c784c4dc))\n* **deps:** Update dependency packaging to v23 ([b81b513](https://github.com/BradenM/micropy-cli/commit/b81b513248e82bb14e7d24e9b528f37fb278942b))\n* **deps:** Update dependency pydantic to v1.10.5 ([0fb9624](https://github.com/BradenM/micropy-cli/commit/0fb96244da1f42207aa09cb2d1230f0c5278a6f6))\n* **deps:** Update dependency tqdm to v4.65.0 ([9fb64dd](https://github.com/BradenM/micropy-cli/commit/9fb64ddb893a48b60ba41d5b25b422a7542c9b09))\n* **deps:** Update dependency typing-extensions to v4.5.0 ([e6c57c3](https://github.com/BradenM/micropy-cli/commit/e6c57c30bf7f73b887437950d72593594f6f08ac))\n* **pyd:** Backend rshell excess consumer kwarg, can't union with supported py versions. ([de8da4e](https://github.com/BradenM/micropy-cli/commit/de8da4e46d3033d4a1d8c25452d975a26ed1892d))\n\n\n### Miscellaneous Chores\n\n* **release:** Update release. ([7b6d9bb](https://github.com/BradenM/micropy-cli/commit/7b6d9bb90f8d003627e57e5a8d3e70bba2e104d9))\n\n## [4.1.0-beta](https://github.com/BradenM/micropy-cli/compare/v4.0.0...v4.1.0-beta) (2023-01-30)\n\n\n### Features\n\n* **cli:** Add flag to show outdated stub packages in search + group output by repo. ([e2cdff7](https://github.com/BradenM/micropy-cli/commit/e2cdff7b0642e461182704835a6c826693c0deca))\n* **cli:** Format repo as title in stubs search output. ([eaf0543](https://github.com/BradenM/micropy-cli/commit/eaf054307f4669ab1578b8bc090fbd9c1b42ab7a))\n* **cli:** Improve stub search output. ([4c127ac](https://github.com/BradenM/micropy-cli/commit/4c127ac5dab299e91a56fa2d675b771b45a79cdd))\n* **cli:** Utilize stub source locators during add. ([d24b409](https://github.com/BradenM/micropy-cli/commit/d24b4095c168184025be5736aee4fbc69427c6df))\n* **data:** Add display names for current stub sources. ([7f6b2cd](https://github.com/BradenM/micropy-cli/commit/7f6b2cd4afe9bcea1b76029a2f94d96bc3d18de8))\n* **data:** Add micropython-stubs source ([de9c2e2](https://github.com/BradenM/micropy-cli/commit/de9c2e2c987422d4871b60ab0cd7ade624c387d8))\n* **deps:** Add attrs/pydantic ([06660f0](https://github.com/BradenM/micropy-cli/commit/06660f0bdaf6c06e3787dd170e7d47bd036c3722))\n* **deps:** Add distlib. ([fab22ba](https://github.com/BradenM/micropy-cli/commit/fab22ba2e2fbccf791e64f825dc3545ef2c5606d))\n* **deps:** Add importlib_metadata as dep. ([6acf3ca](https://github.com/BradenM/micropy-cli/commit/6acf3ca11f9dd7444aa2e39fd0a17fe52cb3226c))\n* **deps:** Add pytest-clarity+better-exceptions to dev deps. ([dc9d958](https://github.com/BradenM/micropy-cli/commit/dc9d9587c5acab63fe5d6deb6b1e9e29716ee9e0))\n* **main:** Drop in new StubRepository impl in place of StubRepo. ([25f0402](https://github.com/BradenM/micropy-cli/commit/25f0402fe65420337d9dffcc1a7abdc3962f2f1f))\n* **main:** Init `StubRepository` as attr. ([c17be65](https://github.com/BradenM/micropy-cli/commit/c17be65e532c04bbaddd00f1d38c01abb8f1e2ff))\n* **pkg:** Add __main__ module entry. ([2388858](https://github.com/BradenM/micropy-cli/commit/238885848cf47d4dadab907130a3c715341bb9d9))\n* **pkg:** Cleanup package entry, dynamically resolve version. ([72ea665](https://github.com/BradenM/micropy-cli/commit/72ea66584387bc3f0db85d9042d14b9ad676884e))\n* **project:** Add pylance settings to vscode template. ([bbdc936](https://github.com/BradenM/micropy-cli/commit/bbdc936c5885dfe80fde7676344ce97cada6807f))\n* **project:** Assume pylance until proper refactorings can be done. ([2610c2a](https://github.com/BradenM/micropy-cli/commit/2610c2a38b83c0d525e3a31c4ff0d40fc88a7ea7))\n* **stubs:** `RepoStubLocator` locate strategy. ([08f8f86](https://github.com/BradenM/micropy-cli/commit/08f8f863b2fe8b5eb75aa88374935299d03ba6c5))\n* **stubs:** Accept generic package type in stub manifest ([9d17331](https://github.com/BradenM/micropy-cli/commit/9d173316f0dc7da5479523b82159d461ee990b46))\n* **stubs:** Add `display_name` field to stub repository. ([a3ef03f](https://github.com/BradenM/micropy-cli/commit/a3ef03f50b79cb0139c4b0423c86f32d3c1acd30))\n* **stubs:** Add `resolve_package_(absolute,)_versioned_name` to manifest. ([37bbfa6](https://github.com/BradenM/micropy-cli/commit/37bbfa678bb29cc86f1a202ec994f9e40fb6e0fa))\n* **stubs:** Add method for resolving absolute stub package name from manifest. ([ad55507](https://github.com/BradenM/micropy-cli/commit/ad55507a6b19b5273ef0f9c44d1112faa7b151c9))\n* **stubs:** Add MicropyStubs package/manifest models. ([021c279](https://github.com/BradenM/micropy-cli/commit/021c279fdc2b50a55a641761ecfc32bf54398b9b))\n* **stubs:** Add Micropython stubs package/manifest models. ([a9297dc](https://github.com/BradenM/micropy-cli/commit/a9297dcfacbd5a31e7207b43300cd4376d9ce089))\n* **stubs:** Add RepositoryInfo model. ([109aed3](https://github.com/BradenM/micropy-cli/commit/109aed30e7e588dcbeae405ead174c6b1c26d745))\n* **stubs:** Add resolve package url abstract meth to stubs manifest ([8737f52](https://github.com/BradenM/micropy-cli/commit/8737f529365f556a0d3d1e4ccde1da2e2beae7aa))\n* **stubs:** Add StubPackage model. ([9664111](https://github.com/BradenM/micropy-cli/commit/9664111e9304fa3149da992adbf247ec97587f1b))\n* **stubs:** Add StubRepository for managing stub manifests. ([781f7cd](https://github.com/BradenM/micropy-cli/commit/781f7cddcf3912c8da89e790b717b34f04366737))\n* **stubs:** Add StubRepositoryPackage model. ([e0dda9f](https://github.com/BradenM/micropy-cli/commit/e0dda9f193eca1499f3d42e0df8fb8f2e8ecc0f8))\n* **stubs:** Add StubsManifest model. ([3ae9456](https://github.com/BradenM/micropy-cli/commit/3ae9456a3277e16161f0c8f29616377338afce4c))\n* **stubs:** Assume latest version by default, optionally show latest only in search, general improvements in stub repo. ([b55b483](https://github.com/BradenM/micropy-cli/commit/b55b483d591990f6cdf264e2ad1fcc9f190dddb0))\n* **stubs:** Build progressive package indexes in `StubRepository`, utilize in search/resolve. ([318ec13](https://github.com/BradenM/micropy-cli/commit/318ec13e1fb35e6f757ec426c1f32c712de00f9b))\n* **stubs:** Check absolute name for stub resolve matching. ([142648d](https://github.com/BradenM/micropy-cli/commit/142648d4e2edabd6956723e781f6d7608a5f6030))\n* **stubs:** Enforce faux immutability in StubRepository. ([a17cc5e](https://github.com/BradenM/micropy-cli/commit/a17cc5e66e938edd093ba1e4c7c283ab6d64d074))\n* **stubs:** Expose `repo_name`,`versioned_name`,`absolute_versioned_name` on `StubRepositoryPackage` ([e257aa5](https://github.com/BradenM/micropy-cli/commit/e257aa56d14a98e3b81ef26c5e8fc80615036d93))\n* **stubs:** Expose name/version/absolute_name fields from stub repo package. ([d88dcae](https://github.com/BradenM/micropy-cli/commit/d88dcaea98c30b41ccacd18ae7f4c1b474fa9730))\n* **stubs:** Expose url via StubRepositoryPackage descriptor. ([4fd1b12](https://github.com/BradenM/micropy-cli/commit/4fd1b1202503bed22a34e861df8ff815e5a5363e))\n* **stubs:** Impl `resolve_package_url` for micropython-stubs repo. ([4bd70aa](https://github.com/BradenM/micropy-cli/commit/4bd70aaea7c78ef2ae81bf432f53cef8971d5870))\n* **stubs:** Impl resolve package method in StubRepository. ([c28d988](https://github.com/BradenM/micropy-cli/commit/c28d98815a31cb0c790ea4fdcaa7371f202545b0))\n* **stubs:** Implement dirty metadata adapter for dist-based stubs until proper refactorings. ([a60138f](https://github.com/BradenM/micropy-cli/commit/a60138f73863b3b93e8465a38b3805aec99f88fe))\n* **stubs:** Make `StubPackage` immutable. ([2fab17d](https://github.com/BradenM/micropy-cli/commit/2fab17d46445448c93bce1ca532f112507480f40))\n* **stubs:** Make `StubRepository.resolve_package` return `StubRepositoryPackage` ([24ef2fa](https://github.com/BradenM/micropy-cli/commit/24ef2fa51eafcfc25b7f468288908fef736c4830))\n* **stubs:** Make `StubRepositoryPackage` immutable, iterate matchers. ([ae71f91](https://github.com/BradenM/micropy-cli/commit/ae71f9136aeae81e37f03cfe7d8ea564e64f85cf))\n* **stubs:** Make `StubsManifest` immutable. ([2a6ffa3](https://github.com/BradenM/micropy-cli/commit/2a6ffa3fb57e304cda88cc22c742b454a800db7a))\n* **stubs:** Make `StubSource` proper abstract, add prepare abstractmethod + impls. ([d690e71](https://github.com/BradenM/micropy-cli/commit/d690e71c6c1e9d90dc4414323c6488f7fbd7d540))\n* **stubs:** Make micropython stubs package sortable. ([49b6df0](https://github.com/BradenM/micropy-cli/commit/49b6df03d4b0aeb2ef124a3f2150bb3417019e80))\n* **stubs:** Micropy-stubs resolve package url impl, stub micropython for now. ([c832cb0](https://github.com/BradenM/micropy-cli/commit/c832cb0c82ce3533ae18e90ada0359e375de2e1e))\n* **stubs:** Rename `StubRepositoryPackage.repository` -&gt; `manifest`. ([a8b3ec8](https://github.com/BradenM/micropy-cli/commit/a8b3ec8b72094654cf665bc330e75a55bdf96b72))\n* **stubs:** Support reuse of `StubSource` instances, improvements. ([b873a62](https://github.com/BradenM/micropy-cli/commit/b873a62970264928d772575c84e6c8ead305c6dc))\n* **stubs:** Utilize `StubRepositoryPackage.match_exact` ([3ec08dd](https://github.com/BradenM/micropy-cli/commit/3ec08ddc5aad0bfbd8b98da6388989368d674790))\n* **stubs:** Utilize locators in `StubManager`, resolve requirements from metadata. ([5c19624](https://github.com/BradenM/micropy-cli/commit/5c196249704a14d95ebfc99aa75ae77054cd8c48))\n* **stubs:** Validate RepoInfo source, add method for fetching contents. ([0f7487f](https://github.com/BradenM/micropy-cli/commit/0f7487fa45beb62d149539967b6671c78cc41c60))\n* **utils:** Add SupportsLessThan protocol to types util. ([489a9b0](https://github.com/BradenM/micropy-cli/commit/489a9b041082af18adf929471d7cc8f1f0bd39d5))\n* **utils:** Add types to `ensure_existing_dir` ([e8e6ea8](https://github.com/BradenM/micropy-cli/commit/e8e6ea886098bd7bfceaf084e1d67673b5290177))\n* **utils:** Add utils._compat module, add importlib metadata ([5722504](https://github.com/BradenM/micropy-cli/commit/572250447091e721623e562420c80295da999525))\n* **utils:** Add utils.types, PathStr alias. ([63f65b9](https://github.com/BradenM/micropy-cli/commit/63f65b9b649f8de7b3d244c01a5fe17c201c4ca6))\n* **utils:** Defer updating stale cache with `utils.get_cached_data` ([afd2ba5](https://github.com/BradenM/micropy-cli/commit/afd2ba5932d375a89401d1c314a3d6447a56e53f))\n\n\n### Bug Fixes\n\n* **cli:** Click fails to resolve package version. ([65ef13b](https://github.com/BradenM/micropy-cli/commit/65ef13b3939ffa65beca8eb74fd580cfd31d3382))\n* **compat:** &lt;=3.8 python typing compat issues. ([e7600b4](https://github.com/BradenM/micropy-cli/commit/e7600b42a9f08035187b8644eecc66315619c753))\n* **deps:** Only install import-metadata when py version &lt;3.10 ([ac1356d](https://github.com/BradenM/micropy-cli/commit/ac1356da308cfae94d16cd9935b9733ed0fad67a))\n* **deps:** Pin dependencies ([84aa3c3](https://github.com/BradenM/micropy-cli/commit/84aa3c32db009121fbdd868b9664b648087186d3))\n* **deps:** Pin dependencies ([1b6a46a](https://github.com/BradenM/micropy-cli/commit/1b6a46a828081d31153428e467780216737723a9))\n* **deps:** Update dependency attrs to v22.2.0 ([9435223](https://github.com/BradenM/micropy-cli/commit/9435223ebe70554b6fc8d45ad68a798809ce55e2))\n* **deps:** Update dependency boltons to v21 ([52bd39c](https://github.com/BradenM/micropy-cli/commit/52bd39c989dbbd1c7130ab8702d1c8caa0b50133))\n* **deps:** Update dependency gitpython to v3.1.30 ([f5bb503](https://github.com/BradenM/micropy-cli/commit/f5bb5037aa1965be85198269b2ab8166f660f228))\n* **deps:** Update dependency importlib-metadata to v5.2.0 ([42ab466](https://github.com/BradenM/micropy-cli/commit/42ab46681b5597eeb77d78573adcdabcd6a10bd0))\n* **deps:** Update dependency markupsafe to v2.1.2 ([4239f9b](https://github.com/BradenM/micropy-cli/commit/4239f9bee88681f357b01f09b55b032ce3a5d39c))\n* **deps:** Update dependency pydantic to v1.10.3 ([8d4d64d](https://github.com/BradenM/micropy-cli/commit/8d4d64ddc4ffcb64b578bd04f3f91092f6fc73a4))\n* **deps:** Update dependency pydantic to v1.10.4 ([22dfef1](https://github.com/BradenM/micropy-cli/commit/22dfef18fc60db43564ee7379b319a6bb3a200e4))\n* **deps:** Update dependency python-minifier to v2.8.0 ([9b0b2ef](https://github.com/BradenM/micropy-cli/commit/9b0b2efcfd2f3bc3c3ff6a8ee596329471733bd9))\n* **deps:** Update dependency requests to v2.28.2 ([8e8d259](https://github.com/BradenM/micropy-cli/commit/8e8d259065b3d226aeee7be4ee0ed81cc9c7643d))\n* **deps:** Update dependency requirements-parser to v0.5.0 ([26a8931](https://github.com/BradenM/micropy-cli/commit/26a8931d76121416b06fd8da90ee93e040963594))\n* **main:** Add types to `MicroPy.stubs` ([2340184](https://github.com/BradenM/micropy-cli/commit/2340184db4a6e811377eab8f86f99333b8c16ab6))\n* **main:** StubRepository has faux immutability. ([71feed2](https://github.com/BradenM/micropy-cli/commit/71feed28fa9a94108629784e0ba5f0de3e42ce70))\n* **project:** Bad type union. ([3d32e5c](https://github.com/BradenM/micropy-cli/commit/3d32e5cb3f7802d867a7a9ac7788d33ea7c6f2cd))\n* **stubs:** Ensure src path is path type in log. ([881a6a6](https://github.com/BradenM/micropy-cli/commit/881a6a6401166b621bd5eef0e76cf0b905d0b1dc))\n* **stubs:** Perform repo lookups prior to adding stub ([5410a13](https://github.com/BradenM/micropy-cli/commit/5410a1369d6f693b69e36fc42b25f4a9a23c222a))\n* **stubs:** Remove mutating subclass hook from `StubsManifest`. ([d3fcd7e](https://github.com/BradenM/micropy-cli/commit/d3fcd7eaef30da7e1621a507ba179a52d80ee1cf))\n* **stubs:** Use `typing.Type` for sub py3.7 compat. ([1350263](https://github.com/BradenM/micropy-cli/commit/1350263bcd8b73368d99e849ead50d61b9e479b8))\n* **stubs:** Utilize absolute names in stub search results. ([6c81a93](https://github.com/BradenM/micropy-cli/commit/6c81a93e8596836d13cbf5a8c0554495d5873b6b))\n* **utils:** Add annotations future in type utils. ([d2d0ed8](https://github.com/BradenM/micropy-cli/commit/d2d0ed815cb46c2d7fbca6d9c1408813aa3c8565))\n* **utils:** Remove PathLike GenericAlias subscript for py &lt;3.8 ([e22343a](https://github.com/BradenM/micropy-cli/commit/e22343af79972c90c68de51249f4bd48c43372b1))\n* **utils:** Use importlib metadata to check micropy version in utils. ([dbeb0a9](https://github.com/BradenM/micropy-cli/commit/dbeb0a90ebe3fbe1168968198d799514382682c3))\n\n\n### Documentation\n\n* **chglog:** Remove unreleased for release-please. ([22d7be0](https://github.com/BradenM/micropy-cli/commit/22d7be04ac806653962187aa5e67f28fd07726b9))\n* **conf:** Dynamically determine docs release version ([fc8ab96](https://github.com/BradenM/micropy-cli/commit/fc8ab966f8f4a44031eba42145afc244c521c896))\n\n\n### Code Refactoring\n\n* **stubs:** Remove old `StubRepo` class. ([b9de35a](https://github.com/BradenM/micropy-cli/commit/b9de35ab082b9c2e69d5b130e9e11dc45f843320))\n* **stubs:** Remove search remote from stub manager. ([95d42f0](https://github.com/BradenM/micropy-cli/commit/95d42f0f643e030f4f6e3f3e6770260a445e2014))\n* **stubs:** Update repository impls to retain immutability. ([b44b335](https://github.com/BradenM/micropy-cli/commit/b44b335d1a70421058fdb715fc9d95cf2bd84fae))\n* **stubs:** Utilize locator strategies over stub source factory method. ([e81ac84](https://github.com/BradenM/micropy-cli/commit/e81ac8467c744f8d4462d6dd18d877c906d97773))\n* **utils:** Update usage of importlib metadata. ([a09aaf9](https://github.com/BradenM/micropy-cli/commit/a09aaf9d7e15c61ad58b2a500514e58bedbb8741))\n\n## [v4.0.0] - 2022-11-13\n\n### Bug Fixes\n\n-   **deps:** update dependency python-minifier to v2.7.0\n-   **deps:** update dependency markupsafe to v2.1.1\n-   **deps:** update dependency jinja2 to v3.1.2\n-   **deps:** update dependency gitpython to v3.1.29\n-   **deps:** update dependency colorama to v0.4.6\n-   **deps:** pin dependencies\n\n### Code Refactoring\n\n-   **stubs:** utilize helper method during remote stub unpack.\n-   **utils:** extract helper methods, add types.\n\n### Features\n\n-   **deps:** update python constraint to include v3.11, update lockfile.\n-   **deps:** upgrade to click v8\n-   **deps:** update all deps in-range\n\n<a name=\"v4.0.0-rc.2\"></a>\n\n## [v4.0.0-rc.2] - 2022-04-17\n\n### Bug Fixes\n\n-   **pyd:** remove dict union operator till py3.9 min support\n-   **pyd:** only type-cast rshell if type checking is enabled\n-   **pyd:** capture module not found error during rshell import attempt\n-   **pyd:** upydevice connect proper attr error if before established\n-   **pyd:** use host path suffix check only as fallback in copy_to\n-   **pyd:** consumer handler protocol methods should not be writable\n\n### Features\n\n-   **deps:** add upydevice+deps, missing type-stubs to dev, update mypy config\n-   **deps:** upgrade upydevice and remove prev missing deps\n-   **exc:** add PyDeviceError, PyDeviceConnectionError exceptions\n-   **main:** update to utilize new pyd module\n-   **pkg:** add pypi-test source to pyproject\n-   **pkg:** regenerate changelog\n-   **pkg:** add poetry+local pre-commit hook for docs export\n-   **pkg:** export pyd from pkg root\n-   **pkg:** add git-chlog config\n-   **pyb:** add abcs for PyDevice, MetaPyDevice, Consumer/StreamConsumer\n-   **pyd:** establish should return pyd instance, update consumer types\n-   **pyd:** use/pass consumer handlers via delegate, expose connect/dc\n-   **pyd:** add ConsumerDelegate, StreamHandlers, MessageHandlers\n-   **pyd:** update rshell backend to implement MetaPyDeviceBackend\n-   **pyd:** add PyDeviceConsumer protocol\n-   **pyd:** add PyDevice implementation\n-   **pyd:** update upyd backend to interfaces + cleanup\n-   **pyd:** move tqdm-progress consumer to pyd.consumers\n-   **pyd:** add Stream/Message consumer protocols+handler protos, Split MetaPyDevice/MetaPyDeviceBackend\n-   **pyd:** add upydevice-based pyd backend\n-   **pyd:** add rshell-based pydevice backend\n-   **pyd:** rename pyb module -> pyd\n-   **pyd:** add pyd module explicit exports\n-   **pyd:** allow delegate_cls to be injected to pydevice via init\n-   **scripts:** add script for exporting docs requirements\n-   **utils:** remove pybwrapper\n\n<a name=\"v4.0.0.rc.1\"></a>\n\n## [v4.0.0.rc.1] - 2022-03-14\n\n### Bug Fixes\n\n-   **dev-deps:** update pytest to ^7.0 to resolve py10+win pyreadline crash\n-   **pkg:** rshell markers for win32\n-   **pkg:** fix mistake in rshell marker\n-   **pkg:** do not install rshell when py>=3.10 and on windows due to pyreadline.\n-   **pkg:** win32 rshell python marker\n-   **pkg:** upgrade too and pin jinja2 @ 3.0.3\n-   **project:** report exception on install failure to stdout\n-   **stubber:** replace pyminifer with python-minifer\n-   **utils:** capture attribute err that occurs on py310 win32 rshell import\n-   **utils:** utilize mp-stubbers new logic for generating stubs\n\n### Features\n\n-   **deps:** update dependencies scoped\n-   **deps:** update micropython-stubber to latest master commit\n-   **pkg:** move pytest+coverage cfg to pyproject\n-   **pkg:** add missing packaging dep\n-   **pkg:** update includes to be more strict\n-   **pkg:** restructure and cleanup pyproject with dependency groups\n-   **pkg:** merge create_stubs group into default\n\n<a name=\"v3.6.0\"></a>\n\n## [v3.6.0] - 2021-05-17\n\n### Bug Fixes\n\n-   **data:** update stubs schema for compat with latest stubber\n\n### Features\n\n-   **deps:** update rshell dependency\n-   **deps:** update deps, add micropy-cli w/ extras as dev-dep\n-   **deps:** setup black, pre-commit\n-   **pkg:** update setup file\n-   **pre-commit:** add pre-commit config\n-   **stubber:** update micropython-stubber submodule to latest\n-   **utils:** remove dynamic\n-   **utils:** refactor stub-gen to stubs, dynamically create stubber module for import\n\n<a name=\"v3.5.0\"></a>\n\n## [v3.5.0] - 2020-11-17\n\n<a name=\"v3.5.0.rc.1\"></a>\n\n## [v3.5.0.rc.1] - 2020-11-17\n\n### Bug Fixes\n\n-   full name case mismatch for pypi packages\n-   package installation failures were silent\n-   **pkg:** constrain questionary version to <1.8.0\n-   **pkg:** setuptools editable installation issues\n\n### Features\n\n-   **package:** detect and return VCSDependencySource when needed in create dep source factory\n-   **package:** add VCSDependencySource class for supporting VCS requirements\n-   **package:** add attributes and logic for VCS packages\n-   **pkg:** bump questionary dependency to ^1.8.1\n-   **pkg:** add GitPython dependency\n\n### Reverts\n\n-   chore(deps): update setup.py\n\n<a name=\"v3.4.0\"></a>\n\n## [v3.4.0] - 2020-07-25\n\n### Bug Fixes\n\n-   **deps:** update dpath constraint to >=1.4,<2.0\n\n<a name=\"v3.3.0\"></a>\n\n## [v3.3.0] - 2019-12-23\n\n### Bug Fixes\n\n-   ensure any values to be extended in config are of type list ([#94](https://github.com/BradenM/micropy-cli/issues/94))\n-   **utils:** ignore candidate releases when checking for update\n\n### Features\n\n-   **project:** generate recommended extensions with vscode integration ([#95](https://github.com/BradenM/micropy-cli/issues/95))\n\n<a name=\"v3.2.0\"></a>\n\n## [v3.2.0] - 2019-12-14\n\n<a name=\"v3.2.0.rc.2\"></a>\n\n## [v3.2.0.rc.2] - 2019-12-13\n\n### Bug Fixes\n\n-   Handle Invalid Requirements\n-   **cli:** Handle errors when reading requirements from path\n-   **cli:** Handle and Report Invalid Package Name Error\n-   **deps:** Fix loading requirements from path\n-   **utils:** Follow redirects when testing for valid url\n\n### Code Refactoring\n\n-   **deps:** Remove Exception handling from Packages Module\n\n### Features\n\n-   Add Base and Requirement Exceptions\n-   **poetry:** Update Poetry to Stable\n\n<a name=\"v3.2.0.rc.1\"></a>\n\n## [v3.2.0.rc.1] - 2019-12-09\n\n### Bug Fixes\n\n-   Make rshell and pyminifier requirements optional ([#82](https://github.com/BradenM/micropy-cli/issues/82))\n-   Colorama Version Constraint\n-   Colorama Broken Release, Style\n-   VSCode Settings failed to populate on reload ([#81](https://github.com/BradenM/micropy-cli/issues/81))\n-   **config:** Remove concrete path from ConfigSource\n-   **config:** Remove cache method for better implementation later\n-   **deps:** Temporary Directory would be removed before it was ready\n-   **logger:** Exception formatting\n-   **project:** Context not being updated when needed\n-   **project:** Add empty dict to config on create\n\n### Code Refactoring\n\n-   Cleanup Stubs Module Context Handling\n-   **packages:** Use new Dependency Api in Packages Module\n\n### Features\n\n-   **cli:** Basic install from path option implementation\n-   **config:** Manage sync via callback\n-   **config:** New Interface with file/memory autosync and dot notation\n-   **config:** Dictionary Config Source\n-   **config:** Cache and Root Key Context Manager for Config Items\n-   **config:** Use dpath to handle Config paths and merging\n-   **config:** Improved handling of collection data types\n-   **config:** Add pop method to config\n-   **config:** New and Improved Config File Interface\n-   **context:** Use DictConfig for Project Context\n-   **deps:** Package Class for representing a requirement\n-   **deps:** Address Package Source Uniformly\n-   **deps:** Allow local deps to be sourced from anywhere\n-   **project:** Add local-lib-path config option.\n-   **project:** Load Project Modules by individual Priority\n-   **project:** Update Projects to use Priority Queue\n-   **project:** Implement Dependencies in Project Module\n-   **project:** Render Local Deps in Project Settings\n-   **project:** Update Config/Context automatically\n-   **project:** Update modules to use new, more flexible config\n-   **project:** Use new Config Interface in Projects\n-   **project:** Try to add local deps as relative to project, fallback...\n-   **project:** Replace Project Cache with Config Instance\n-   **template:** Update TemplateModule\n\n### Performance Improvements\n\n-   **size:** Slimmed Package Size\n\n<a name=\"v3.1.1\"></a>\n\n## [v3.1.1] - 2019-12-03\n\n### Bug Fixes\n\n-   HookProxy failed to resolve with kwargs\n-   **checks:** VSCode check failing on py36\n-   **logger:** Exception formatting\n-   **package:** Add metadata to pyproject.toml\n-   **package:** Update Makefile and bump2version to use pyproject\n-   **package:** Use Dephell to generate setup.py, Remove Manifiest.in\n-   **project:** Exception Raised if no Templates are used in Project\n-   **project:** VSCode check always failed silently\n\n### Features\n\n-   Cleanup Log File Formatting\n-   Use Poetry for Dependency Management\n\n### Performance Improvements\n\n-   **size:** Slimmed Package Size\n\n<a name=\"v3.1.0\"></a>\n\n## [v3.1.0] - 2019-11-12\n\n### Bug Fixes\n\n-   Handle Errors when adding Packages\n-   Project Context Stub Path Ordering\n-   HookProxy failed to work with descriptors.\n-   PackagesModule Dev, Project Context\n-   Move Template Check flag to TemplatesModule\n-   Active Project Resolve, Cli Templates List\n\n### Code Refactoring\n\n-   Add Packages from File\n-   Import MicroPy and Modules to Package Root\n-   Restructure Project Module\n\n### Features\n\n-   Report Ready on Project Load, Code Cleanup\n-   Write .gitignore file in generated .micropy folder\n-   Proxy Project Hooks to allow hooks with the same name, Split De...\n-   Resolve Project Hooks via attrs, Fix Stub List\n-   **project:** Project Method Hook Decorator\n\n### Performance Improvements\n\n-   Lazy Load Project Stubs\n\n<a name=\"v3.0.1\"></a>\n\n## [v3.0.1] - 2019-10-13\n\n### Bug Fixes\n\n-   Auto Update Check's Cache not expiring after update\n-   VSCode Template Check always Fails on Linux ([#65](https://github.com/BradenM/micropy-cli/issues/65))\n-   **upstream:** Fails to Generate Stub Files\n\n<a name=\"v3.0.0\"></a>\n\n## [v3.0.0] - 2019-10-13\n\n### Bug Fixes\n\n-   Project Fails to Init due to Checks on Windows\n-   Stub Package Url fails to resolve on Windows\n-   Handle Chunked Content Length on Package Download\n-   Package urls not resolving correctly\n-   Fails to load Project if Template Files are Missing ([#55](https://github.com/BradenM/micropy-cli/issues/55))\n\n### Code Refactoring\n\n-   **data:** Move all Data Paths to Data Module\n\n### Features\n\n-   Add Flag for Skipping Template Checks\n-   Search/Retrieve Stubs Directly from micropy-stubs\n-   Update MicropyCli Stub Sources\n-   Refactor MicroPy Class for Better State Management\n\n### Performance Improvements\n\n-   Lazy Load Stubs when Needed\n-   **project:** Lazy Load Current Active Project\n\n### BREAKING CHANGE\n\nmicropy.STUBS renamed to micropy.stubs\n\n<a name=\"v2.2.0\"></a>\n\n## [v2.2.0] - 2019-09-28\n\n### Features\n\n-   Template Checks, MS-Python Check ([#52](https://github.com/BradenM/micropy-cli/issues/52))\n-   **cli:** Automatic Update Checks ([#54](https://github.com/BradenM/micropy-cli/issues/54))\n-   **vscode:** Ensure Jedi is Disabled in VSCode Template\n\n### Performance Improvements\n\n-   **stubs:** Cache Available Stubs for Searching\n\n<a name=\"v2.1.1\"></a>\n\n## [v2.1.1] - 2019-09-22\n\n### Bug Fixes\n\n-   **hotfix:** Remove workspaceRoot var from VSCode Settings ([#51](https://github.com/BradenM/micropy-cli/issues/51))\n\n### Features\n\n-   Relicensed under MIT\n\n### BREAKING CHANGE\n\nNo longer compatible with <=ms-python.python[@2019](https://github.com/2019).8.30787 VSCode Extension\n\n<a name=\"v2.1.0\"></a>\n\n## [v2.1.0] - 2019-09-01\n\n### Bug Fixes\n\n-   **project:** Requirement Files skipped on First Init\n-   **windows:** Support User Level Directory Linking ([#45](https://github.com/BradenM/micropy-cli/issues/45))\n\n### Features\n\n-   **log:** Cap Log File at 2MB\n-   **project:** Init Project with Micropy Dev Dependency\n-   **project:** Git Ignore Template Option\n\n<a name=\"v2.0.2\"></a>\n\n## [v2.0.2] - 2019-08-21\n\n### Bug Fixes\n\n-   **dep:** Require appropriate Click version\n-   **windows:** Warn User if MicroPy Lacks Admin Privs\n\n<a name=\"v2.0.1\"></a>\n\n## [v2.0.1] - 2019-07-26\n\n### Bug Fixes\n\n-   **stubs:** Reduce Schema Strictness\n\n<a name=\"v2.0.0\"></a>\n\n## [v2.0.0] - 2019-07-25\n\n### Bug Fixes\n\n-   **dep:** Broken Docutils Dependency\n-   **project:** Only modules install correctly\n\n### Features\n\n-   Add Optional Pyminifier Dep for Stub Creation\n-   **cli:** Install Python Packages for Project\n-   **cli:** Verbosity Flag for Stub Creation\n-   **dep:** Update Tox to latest\n-   **dep:** Packaging Module Requirement\n-   **lib:** Update Stubber to Process Branch\n-   **project:** Update requirements.txt Files on Install\n-   **project:** Template Update Functionality\n-   **project:** Install from Requirements.txt\n-   **project:** Retrieve and Stub Project Requirements\n-   **project:** Project Config in Info File\n-   **project:** Make Templates Optional via CLI ([#30](https://github.com/BradenM/micropy-cli/issues/30))\n-   **pyb:** Handle Pyboard Output and Errors\n-   **stubs:** Minify Stubber Before Executing\n-   **util:** Generate Stub from File Utility\n\n<a name=\"v1.1.3\"></a>\n\n## [v1.1.3] - 2019-07-20\n\n### Bug Fixes\n\n-   ValueError raised after Creating Project in Windows ([#33](https://github.com/BradenM/micropy-cli/issues/33))\n-   Unicode Error raised when logging on Windows ([#32](https://github.com/BradenM/micropy-cli/issues/32))\n\n<a name=\"v1.1.2\"></a>\n\n## [v1.1.2] - 2019-07-19\n\n### Bug Fixes\n\n-   **stubs:** Ensure Firmware Stubs Load First\n\n<a name=\"v1.1.1\"></a>\n\n## [v1.1.1] - 2019-07-17\n\n### Bug Fixes\n\n-   Temp Hotfix for False Stub Duplication\n\n<a name=\"v1.1.0\"></a>\n\n## [v1.1.0] - 2019-07-16\n\n### Bug Fixes\n\n-   **cli:** Stub List always prints Unknown\n-   **cli:** Made Stub Search Case Insensitive\n-   **stubs:** FileExistsError when adding existing Stub\n\n### Features\n\n-   **cli:** List Project Stubs if in Project Directory\n-   **cli:** Stubs now list by Firmware\n-   **cli:** Create Formatted Strings from Logger\n-   **cli:** Added --force flag when adding stubs\n-   **project:** Micropy Project Info File ([#29](https://github.com/BradenM/micropy-cli/issues/29))\n-   **project:** Micropy Project Folder ([#28](https://github.com/BradenM/micropy-cli/issues/28))\n\n<a name=\"v1.0.0\"></a>\n\n## [v1.0.0] - 2019-07-11\n\n### Bug Fixes\n\n-   **cli:** Init Crashes if no Stubs are Loaded\n-   **cli:** Create Stubs Help Formatting\n-   **log:** Output Highlight Bug, Cleanup\n-   **stub:** Stub Name without Firmware\n-   **stubs:** Firmware not showing as Installed in Stub Search\n-   **stubs:** Fix Existing Firmware Reinstall\n\n### Features\n\n-   Implemented Local and Remote Stub Sources ([#18](https://github.com/BradenM/micropy-cli/issues/18))\n-   **cli:** Minified Cli Output Style\n-   **cli:** Search Available Stubs ([#27](https://github.com/BradenM/micropy-cli/issues/27))\n-   **cli:** Stream Downloads with Progress Bar\n-   **stub:** Update Stubs to Use New Stubber Schema ([#23](https://github.com/BradenM/micropy-cli/issues/23))\n-   **stubs:** Updated micropython-stubber to latest\n-   **stubs:** Add Firmware Frozen Modules to Templates\n-   **stubs:** Device Stubs Firmware Resolution ([#25](https://github.com/BradenM/micropy-cli/issues/25))\n-   **stubs:** Add Device Frozen Modules to Templates ([#24](https://github.com/BradenM/micropy-cli/issues/24))\n-   **stubs:** Added Stub Stdout Verbosity\n-   **stubs:** Add Stubs from Repositories ([#21](https://github.com/BradenM/micropy-cli/issues/21))\n-   **stubs:** Replaced Stubs with Stub \"Packages\"\n-   **stubs:** Stub Repositories ([#20](https://github.com/BradenM/micropy-cli/issues/20))\n-   **stubs:** Update Stub Creation ([#26](https://github.com/BradenM/micropy-cli/issues/26))\n-   **util:** Generic Utility Functions and Module Cleanup\n\n### Performance Improvements\n\n-   **cli:** Only Instantiate MicroPy when needed\n\n<a name=\"v0.3.0\"></a>\n\n## [v0.3.0] - 2019-06-25\n\n### Code Refactoring\n\n-   MicroPy to use new Stub and Utility Features ([#14](https://github.com/BradenM/micropy-cli/issues/14))\n\n### Features\n\n-   **cli:** Version Flag\n-   **log:** New Cli Output Style, Log Class Methods\n-   **pyb:** PyboardWrapper Utility ([#13](https://github.com/BradenM/micropy-cli/issues/13))\n-   **stubs:** Stub Manager ([#5](https://github.com/BradenM/micropy-cli/issues/5))\n-   **utils:** Utils Module and Validator Utility ([#4](https://github.com/BradenM/micropy-cli/issues/4))\n\n<a name=\"v0.2.0\"></a>\n\n## [v0.2.0] - 2019-06-14\n\n### Features\n\n-   **log:** Added Proper Log Formatting, cleaned messages before write.\n-   **log:** Added Logging to Template Module\n-   **project:** Drop Cookiecutter for Purely Jinja2 ([#3](https://github.com/BradenM/micropy-cli/issues/3))\n\n<a name=\"v0.1.1\"></a>\n\n## [v0.1.1] - 2019-06-10\n\n### Bug Fixes\n\n-   **setup:** Fixed missing cookiecutter package requirement\n-   **setup:** Fixed Pypi misinformation, cleaned up dist-management files\n-   **setup:** Fix Missing .vscode Template Files\n\n<a name=\"v0.1.0\"></a>\n\n## v0.1.0 - 2019-06-09\n\n### Bug Fixes\n\n-   Fails First Time Setup Failed to init on first run if the stubs folder didn't exist\n-   Removed old command\n-   Fix Project Init\n-   Added rshell to setup.py\n-   Quick Fix before Project Class Restructure\n-   Packaging Fixes\n-   **package:** Allow multiple versions of python, Update Reqs\n-   **setup:** Included Template in Manifest\n-   **stub:** Fixed Refresh Stubs\n-   **stubs:** Cleaned Stub Names before Adding\n-   **stubs:** Removed Old Stub Command\n-   **stubs:** Fixed missing logging.py\n-   **template:** Fixed src template\n\n### Code Refactoring\n\n-   Setup as proper package\n\n### Features\n\n-   Project Init and Template Serialization\n-   Finished Package Setup and Structure\n-   Let Stub class handle validation and files\n-   Setup Template Files\n-   Initial commit\n-   Add Josverl Stubs on First Setup, Restructured MicroPy\n-   Added MicroPy Parent Class\n-   Added stubber as submodule over pulling files with requests\n-   **log:** Added Silet Stdout Context Manager to Logger\n-   **log:** Setup ServiceLog to work as a single parent Logger with ch...\n-   **log:** Added Logging\n-   **log:** Setup Logger as Borg for easy access\n-   **log:** Added file logging to ServiceLog, Added docs\n-   **project:** Project Module Rewrite to use Cookiecutter and JSON\n-   **pylint:** Added checkbox to choose stubs for pylint\n-   **stub:** Pass Multiple Stubs to .pylintrc\n-   **stub:** Added stub add, refresh commands\n-   **stub:** Added createstub.py download\n-   **stub:** Added Stub Class, Moved Stub logic to MicroPy/Stub\n-   **stubs:** Added Automated Stub Creation on PyBoard\n-   **stubs:** Added Stub Validation, Stub Class Restructure\n-   **stubs:** Added Basic Stub Exceptions\n-   **template:** Setup Template in Cookiecutter Fashion\n\n[v4.0.0]: https://github.com/BradenM/micropy-cli/compare/v4.0.0-rc.2...v4.0.0\n[v4.0.0-rc.2]: https://github.com/BradenM/micropy-cli/compare/v4.0.0.rc.1...v4.0.0-rc.2\n[v4.0.0.rc.1]: https://github.com/BradenM/micropy-cli/compare/v3.6.0...v4.0.0.rc.1\n[v3.6.0]: https://github.com/BradenM/micropy-cli/compare/v3.5.0...v3.6.0\n[v3.5.0]: https://github.com/BradenM/micropy-cli/compare/v3.5.0.rc.1...v3.5.0\n[v3.5.0.rc.1]: https://github.com/BradenM/micropy-cli/compare/v3.4.0...v3.5.0.rc.1\n[v3.4.0]: https://github.com/BradenM/micropy-cli/compare/v3.3.0...v3.4.0\n[v3.3.0]: https://github.com/BradenM/micropy-cli/compare/v3.2.0...v3.3.0\n[v3.2.0]: https://github.com/BradenM/micropy-cli/compare/v3.2.0.rc.2...v3.2.0\n[v3.2.0.rc.2]: https://github.com/BradenM/micropy-cli/compare/v3.2.0.rc.1...v3.2.0.rc.2\n[v3.2.0.rc.1]: https://github.com/BradenM/micropy-cli/compare/v3.1.1...v3.2.0.rc.1\n[v3.1.1]: https://github.com/BradenM/micropy-cli/compare/v3.1.0...v3.1.1\n[v3.1.0]: https://github.com/BradenM/micropy-cli/compare/v3.0.1...v3.1.0\n[v3.0.1]: https://github.com/BradenM/micropy-cli/compare/v3.0.0...v3.0.1\n[v3.0.0]: https://github.com/BradenM/micropy-cli/compare/v2.2.0...v3.0.0\n[v2.2.0]: https://github.com/BradenM/micropy-cli/compare/v2.1.1...v2.2.0\n[v2.1.1]: https://github.com/BradenM/micropy-cli/compare/v2.1.0...v2.1.1\n[v2.1.0]: https://github.com/BradenM/micropy-cli/compare/v2.0.2...v2.1.0\n[v2.0.2]: https://github.com/BradenM/micropy-cli/compare/v2.0.1...v2.0.2\n[v2.0.1]: https://github.com/BradenM/micropy-cli/compare/v2.0.0...v2.0.1\n[v2.0.0]: https://github.com/BradenM/micropy-cli/compare/v1.1.3...v2.0.0\n[v1.1.3]: https://github.com/BradenM/micropy-cli/compare/v1.1.2...v1.1.3\n[v1.1.2]: https://github.com/BradenM/micropy-cli/compare/v1.1.1...v1.1.2\n[v1.1.1]: https://github.com/BradenM/micropy-cli/compare/v1.1.0...v1.1.1\n[v1.1.0]: https://github.com/BradenM/micropy-cli/compare/v1.0.0...v1.1.0\n[v1.0.0]: https://github.com/BradenM/micropy-cli/compare/v0.3.0...v1.0.0\n[v0.3.0]: https://github.com/BradenM/micropy-cli/compare/v0.2.0...v0.3.0\n[v0.2.0]: https://github.com/BradenM/micropy-cli/compare/v0.1.1...v0.2.0\n[v0.1.1]: https://github.com/BradenM/micropy-cli/compare/v0.1.0...v0.1.1\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Braden Mars\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: clean clean-test clean-pyc clean-build\nbold := $(shell tput bold)\nrsttxt := $(shell tput sgr0)\n\n\nclean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts\n\nclean-build: ## remove build artifacts\n\t@printf '$(bold)Cleaning Artifacts...\\n$(rsttxt)'\n\trm -fr build/\n\trm -fr dist/\n\trm -fr .eggs/\n\trm -fr pip-wheel-metadata/\n\tfind . -name '*.egg-info' -exec rm -fr {} +\n\tfind . -name '*.egg' -exec rm -f {} +\n\nclean-pyc: ## remove Python file artifacts\n\tfind . -name '*.pyc' -exec rm -f {} +\n\tfind . -name '*.pyo' -exec rm -f {} +\n\tfind . -name '*~' -exec rm -f {} +\n\tfind . -name '__pycache__' -exec rm -fr {} +\n\nclean-test: ## remove test and coverage artifacts\n\t- rm -fr .tox/\n\t- rm -f .coverage\n\t- rm -fr htmlcov/\n\tfind . -name '.pytest_cache' -exec rm -fr {} +\n\t- rm -f .testmondata\n\t- rm -rf .tmontmp\n\t- rm cov.xml test_log.xml\n\ntest: ## run tests quickly with the default Python\n\tpytest\n\nwatch-build: clean ## build pytest-testmon db\n\tpytest --testmon -c pyproject.toml\n\nwatch: clean ## watch tests\n\t- pytest --testmon\n\tptw --spool 2000 --onpass \"make watch-cov\" --clear -- --testmon -vv -c pyproject.toml\n\nwatch-cov: ## watch test coverage\n\tpytest -n'auto' --forked --cov --cov-append --cov-config=pyproject.toml --cov-report=xml:cov.xml --cov-report term\n\ncoverage: ## generate coverage\n\tpytest -n'auto' --cov --cov-config=pyproject.toml\n\ncoverage-html: ## generate coverage html\n\tpytest -n'auto' --cov --cov-config=pyproject.toml --cov-report html\n\ngendoc: ## Generate Docs\n\t$(MAKE) -C docs clean\n\t$(MAKE) -C docs html\n\t@printf '$(bold)Docs Generated!\\n$(rsttxt)'\n\ntest-release: dist ## release on pypi-test repo\n\t@printf '$(bold)Uploading Test Release to TestPyPi...\\n$(rsttxt)'\n\tpoetry publish -r test\n\t@printf '$(bold)Test Released published!\\n$(rsttxt)'\n\nrelease: dist ## package and release\n\t@printf '$(bold)Uploading package to PyPi...\\n$(rsttxt)'\n\tpoetry publish\n\tgit push --tags\n\t@printf '$(bold)Done! Tags Pushed!\\n$(rsttxt)'\n\ndist: clean ## builds package\n\t@printf '$(bold)Building Source and Wheel...\\n$(rsttxt)'\n\t- rm README.rst\n\tpoetry build\n\tls -l dist\n\ninstall: clean ## install pkg\n\tpython setup.py install\n"
  },
  {
    "path": "README.md",
    "content": "# Micropy Cli [![PyPI][pypi-img]][pypi-url] [![PyPI - Python Version][pypiv-img]][pypi-url] [![Github - Test Micropy Cli][build-img]][build-url] [![Coverage Status][cover-img]][cover-url]\n\n\nMicropy Cli is a project management/generation tool for writing [Micropython](https://micropython.org/) code in modern IDEs such as VSCode.\nIts primary goal is to automate the process of creating a workspace complete with:\n\n* **Linting** compatible with Micropython\n* VSCode **Intellisense**\n* **Autocompletion**\n* Dependency Management\n* VCS Compatibility\n\n\n<p align='center'>\n    <img width='95%' src='.github/img/micropy.svg' alt=\"Micropy Demo SVG\">\n</p>\n\n[pypi-img]: https://img.shields.io/pypi/v/micropy-cli?logo=pypi&logoColor=white&style=flat-square\n[pypi-url]: https://pypi.org/project/micropy-cli/\n[pypiv-img]: https://img.shields.io/pypi/pyversions/micropy-cli.svg?style=flat-square&logo=python&logoColor=green\n[build-img]: https://img.shields.io/github/workflow/status/BradenM/micropy-cli/Test%20MicropyCli/master?logo=github&style=flat-square\n[build-url]: https://github.com/BradenM/micropy-cli/actions\n[cover-img]: https://img.shields.io/coveralls/github/BradenM/micropy-cli/master?style=flat-square&logo=coveralls\n[cover-url]: https://coveralls.io/github/BradenM/micropy-cli\n\n# Getting Started\n\n## Installation\n\nYou can download and install the latest version of this software from the Python package index (PyPI) as follows:\n\n`pip install --upgrade micropy-cli`\n\nIf applicable, you can test out a pre-release by executing:\n\n`pip install --upgrade --pre micropy-cli`\n\n\n\n## Creating a Project\n\nCreating a new project folder is as simple as:\n\n1. Executing `micropy init <PROJECT NAME>`\n2. Selecting which features to enable\n3. Selecting your target device/firmware\n4. Boom. Your workspace is ready.\n\n<p align='center'>\n    <img src='https://github.com/BradenM/micropy-cli/raw/master/.github/img/demo.gif' alt=\"Micropy Demo\">\n</p>\n\n\n## Micropy Project Environment\n\nWhen creating a project with `micropy-cli`, two special items are added:\n\n* A `.micropy/` folder\n* A `micropy.json` file\n\nThe `.micropy/` contains symlinks from your project to your `$HOME/.micropy/stubs` folder. By doing this, micropy can reference the required stub files for your project as relative to it, rather than using absolute paths to `$HOME/.micropy`. How does this benefit you? Thanks to this feature, you can feel free to push common setting files such as `settings.json` and `.pylint.rc` to your remote git repository. This way, others who clone your repo can achieve a matching workspace in their local environment.\n\n> Note: The generated `.micropy/` folder should be *IGNORED* by your VCS. It is created locally for each environment via the `micropy.json` file.\n\nThe `micropy.json` file contains information micropy needs in order to resolve your projects required files when other clone your repo. Think of it as a `package.json` for micropython.\n\n## Cloning a Micropy Environment\n\nTo setup a Micropy environment locally, simply:\n\n* Install `micropy-cli`\n* Navigate to the project directory\n* Execute `micropy`\n\nMicropy will automatically configure and install any stubs required by a project thanks to its `micropy.json` file.\n\n## Project Dependencies\n\nWhile all modules that are included in your targeted micropython firmware are available with autocompletion, intellisense, and linting, most projects require external dependencies.\n\nCurrently, handling dependencies with micropython is a bit tricky. Maybe you can install a cpython version of your requirement? Maybe you could just copy and paste it? What if it needs to be frozen?\n\nMicropy handles all these issues for you automatically. Not only does it track your project's dependencies, it keeps both `requirements.txt` and `dev-requirements.txt` updated, enables autocompletion/intellisense for each dep, and allows you to import them just as you would on your device.\n\nThis allows you to include your requirement however you want, whether that be as a frozen module in your custom built firmware, or simply in the `/lib` folder on your device.\n\n#### Installing Packages\n\nTo add a package as a requirement for your project, run:\n\n`micropy install <PACKAGE_NAMES>`\n\nwhile in your project's root directory.\n\nThis will automatically execute the following:\n\n* Source `PACKAGE_NAMES` from pypi, as a url, or a local path\n* Retrieve the module/package and stub it, adding it to your local `.micropy` folder.\n* Add requirement to your `micropy.json`\n* Update `requirements.txt`\n\nTo install dev packages that are not needed on your device, but are needed for local development, add the `--dev` flag. This will do everything above **except** stub the requirement.\n\nYou can also install all requirements found in `micropy.json`/`requirements.txt`/`dev-requirements.txt` by executing `micropy install` without passing any packages. Micropy will automatically do this when setting up a local environment of an existing micropy project.\n\n#### Example\n\nLets say your new project will depend on [picoweb](https://pypi.org/project/picoweb/) and [blynklib](https://pypi.org/project/blynklib/). Plus, you'd like to use [rshell](https://pypi.org/project/rshell/) to communicate directly with your device. After creating your project via `micropy init`, you can install your requirements as so:\n\n<p align='center'>\n    <img width=\"70%\" src='.github/img/install_demo.svg' alt=\"Micropy Pkg Install Demo\">\n</p>\n\nNow you or anybody cloning your project can import those requirements normally, and have the benefits of all the features micropy brings:\n\n<p align='center'>\n    <img width=\"70%\" src='https://github.com/BradenM/micropy-cli/raw/master/.github/img/deps_demo.gif' alt=\"Micropy Deps Demo\">\n</p>\n\n\n## Stub Management\n\nStub files are the magic behind how micropy allows features such as linting, Intellisense, and autocompletion to work. To achieve the best results with MicropyCli, its important that you first add the appropriate stubs for the device/firmware your project uses.\n\n> Note: When working in a micropy project, all stub related commands will also be executed on the active project. (i.e if in a project and you run `micropy stubs add <stub-name>`, then that stub retrieved AND added to the active project.)\n\n### Adding Stubs\n\nAdding stubs to Micropy is a breeze. Simply run: `micropy stubs add <STUB_NAME>`\nBy sourcing [micropy-stubs](https://github.com/BradenM/micropy-stubs), MicroPy has several premade stub packages to choose from.\n\nThese packages generally use the following naming schema:\n\n`<device>-<firmware>-<version>`\n\nFor example, running `micropy stubs add esp32-micropython-1.11.0` will install the following:\n* Micropython Specific Stubs\n* ESP32 Micropython v1.11 Device Specific Stubs\n* Frozen Modules for both device and firmware\n\nYou can search stubs that are made available to Micropy via `micropy stubs search <QUERY>`\n\nAlternatively, using `micropy stubs add <PATH>`, you can manually add stubs to Micropy.\nFor manual stub generation, please see [Josvel/micropython-stubber](https://github.com/Josverl/micropython-stubber).\n\n### Creating Stubs\n\nUsing `micropy stubs create <PORT/IP_ADDRESS>`, MicropyCli can automatically generate and add stubs from any Micropython device you have on hand. This can be done over both USB and WiFi.\n\n> Note: For stub creation, micropy-cli has additional dependencies.\n>\n> These can be installed by executing: `pip install micropy-cli[create_stubs]`\n\n\n### Viewing Stubs\n\nTo list stubs you have installed, simply run `micropy stubs list`.\n\nTo search for stubs for your device, use `micropy stubs search <QUERY>`.\n\n# See Also\n\n* [VSCode IntelliSense, Autocompletion & Linting capabilities][lemariva-blog]\n    - An awesome article written by [lemariva](https://github.com/lemariva). It covers creating a micropython project environment from scratch using `micropy-cli` and [pymakr-vsc](pymakr-vsc). Great place to start if you're new to this!\n\n* [Developing for the Raspberry Pi Pico in VS Code][cpwood-medium]\n    - A getting started guide for developing in micropython on the Raspberry Pi Pico by [cpwood][cpwood-git].\n    - Also see: [Pico-Go: Micropy-Cli][cpwood-picogo]\n\n* [Awesome MicroPython][awesome-micropy]\n    - Collection of awesome micropython libraries / resources.\n    - Features `micropy-cli` along with several other great development tools under the [Development][awesome-micropy-develop] category.\n\n\n[lemariva-blog]: https://lemariva.com/blog/2019/08/micropython-vsc-ide-intellisense\n[lemariva-git]:  https://github.com/lemariva\n\n[cpwood-medium]: https://medium.com/all-geek-to-me/developing-for-the-raspberry-pi-pico-in-vs-code-getting-started-6dbb3da5ba97\n[cpwood-picogo]: http://pico-go.net/docs/help/micropy/\n[cpwood-git]: https://github.com/cpwood/\n\n[awesome-micropy]: https://awesome-micropython.com/\n[awesome-micropy-develop]: https://awesome-micropython.com/#development\n\n# Acknowledgements\n\n## Micropython-Stubber\n[Josvel/micropython-stubber](https://github.com/Josverl/micropython-stubber)\n\nJosverl's Repo is full of information regarding Micropython compatibility with VSCode and more. To find out more about how this process works, take a look at it.\n\nmicropy-cli and [micropy-stubs](https://github.com/BradenM/micropy-stubs) depend on micropython-stubber for its ability to generate frozen modules, create stubs on a pyboard, and more.\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/_autosummary/micropy.config.config_source.rst",
    "content": "micropy.config.config\\_source\n=============================\n\n.. automodule:: micropy.config.config_source\n\n\n\n\n\n\n\n   .. rubric:: Classes\n\n   .. autosummary::\n\n      ConfigSource\n"
  },
  {
    "path": "docs/_autosummary/micropy.config.rst",
    "content": "micropy.config\n==============\n\n.. automodule:: micropy.config\n\n\n\n\n\n\n\n   .. rubric:: Classes\n\n   .. autosummary::\n\n      Config\n      JSONConfigSource\n      DictConfigSource\n"
  },
  {
    "path": "docs/_autosummary/micropy.exceptions.rst",
    "content": "micropy.exceptions\n==================\n\n.. automodule:: micropy.exceptions\n\n   \n   \n   \n\n   \n   \n   \n\n   \n   \n   .. rubric:: Exceptions\n\n   .. autosummary::\n   \n      StubError\n      StubNotFound\n      StubValidationError\n   \n   \n"
  },
  {
    "path": "docs/_autosummary/micropy.main.rst",
    "content": "micropy.main\n============\n\n.. automodule:: micropy.main\n\n\n\n\n\n\n\n   .. rubric:: Classes\n\n   .. autosummary::\n\n      MicroPy\n"
  },
  {
    "path": "docs/_autosummary/micropy.packages.rst",
    "content": "micropy.packages\n================\n\n.. automodule:: micropy.packages\n\n\n\n   .. rubric:: Functions\n\n   .. autosummary::\n\n      create_dependency_source\n\n\n\n\n\n   .. rubric:: Classes\n\n   .. autosummary::\n\n      LocalDependencySource\n      Package\n      PackageDependencySource\n"
  },
  {
    "path": "docs/_autosummary/micropy.project.modules.rst",
    "content": "micropy.project.modules\n=======================\n\n.. automodule:: micropy.project.modules\n\n\n\n\n\n\n\n   .. rubric:: Classes\n\n   .. autosummary::\n\n      DevPackagesModule\n      PackagesModule\n      ProjectModule\n      StubsModule\n      TemplatesModule\n      HookProxy\n"
  },
  {
    "path": "docs/_autosummary/micropy.project.rst",
    "content": "micropy.project\n===============\n\n.. automodule:: micropy.project\n\n   \n   \n   \n\n   \n   \n   .. rubric:: Classes\n\n   .. autosummary::\n   \n      Project\n   \n   \n\n   \n   \n   \n"
  },
  {
    "path": "docs/_autosummary/micropy.rst",
    "content": "micropy\n=======\n\n.. automodule:: micropy\n\n   \n   \n   \n\n   \n   \n   \n\n   \n   \n   \n"
  },
  {
    "path": "docs/_autosummary/micropy.stubs.rst",
    "content": "micropy.stubs\n=============\n\n.. automodule:: micropy.stubs\n\n\n\n\n\n\n\n   .. rubric:: Classes\n\n   .. autosummary::\n\n      StubManager\n      source\n"
  },
  {
    "path": "docs/_autosummary/micropy.stubs.source.rst",
    "content": "micropy.stubs.source\n====================\n\n.. automodule:: micropy.stubs.source\n\n\n\n   .. rubric:: Functions\n\n   .. autosummary::\n\n      get_source\n\n\n\n\n\n   .. rubric:: Classes\n\n   .. autosummary::\n\n      LocalStubSource\n      RemoteStubSource\n      StubRepo\n      StubSource\n"
  },
  {
    "path": "docs/_autosummary/micropy.utils.rst",
    "content": "micropy.utils\n=============\n\n.. automodule:: micropy.utils\n\n\n\n   .. rubric:: Functions\n\n   .. autosummary::\n\n      create_dir_link\n      ensure_existing_dir\n      ensure_valid_url\n      extract_tarbytes\n      generate_stub\n      get_package_meta\n      get_url_filename\n      is_dir_link\n      is_downloadable\n      is_existing_dir\n      is_url\n      iter_requirements\n      search_xml\n      stream_download\n      is_update_available\n      get_cached_data\n      get_class_that_defined_method\n\n\n\n\n\n   .. rubric:: Classes\n\n   .. autosummary::\n\n      PyboardWrapper\n      Validator\n"
  },
  {
    "path": "docs/base.md",
    "content": "## Installation\n\nYou can download and install the latest version of this software from the Python package index (PyPI) as follows:\n\n`pip install --upgrade micropy-cli`\n\nIf applicable, you can test out a pre-release by executing:\n\n`pip install --upgrade --pre micropy-cli`\n\n\n# Getting Started\n\n## Creating a Project\n\nCreating a new project folder is as simple as:\n\n1. Executing `micropy init <PROJECT NAME>`\n2. Selecting which features to enable\n3. Selecting your target device/firmware\n4. Boom. Your workspace is ready.\n\n<p align='center'>\n    <img src='https://github.com/BradenM/micropy-cli/raw/master/.github/img/demo.gif' alt=\"Micropy Demo\">\n</p>\n\n\n## Micropy Project Environment\n\nWhen creating a project with `micropy-cli`, two special items are added:\n\n* A `.micropy/` folder\n* A `micropy.json` file\n\nThe `.micropy/` contains symlinks from your project to your `$HOME/.micropy/stubs` folder. By doing this, micropy can reference the required stub files for your project as relative to it, rather than using absolute paths to `$HOME/.micropy`. How does this benefit you? Thanks to this feature, you can feel free to push common setting files such as `settings.json` and `.pylint.rc` to your remote git repository. This way, others who clone your repo can achieve a matching workspace in their local environment.\n\n> Note: The generated `.micropy/` folder should be *IGNORED* by your VCS. It is created locally for each environment via the `micropy.json` file.\n\nThe `micropy.json` file contains information micropy needs in order to resolve your projects required files when other clone your repo. Think of it as a `package.json` for micropython.\n\n## Cloning a Micropy Environment\n\nTo setup a Micropy environment locally, simply:\n\n* Install `micropy-cli`\n* Navigate to the project directory\n* Execute `micropy`\n\nMicropy will automatically configure and install any stubs required by a project thanks to its `micropy.json` file.\n\n## Project Dependencies\n\nWhile all modules that are included in your targeted micropython firmware are available with autocompletion, intellisense, and linting, most projects require external dependencies.\n\nCurrently, handling dependencies with micropython is a bit tricky. Maybe you can install a cpython version of your requirement? Maybe you could just copy and paste it? What if it needs to be frozen?\n\nMicropy handles all these issues for you automatically. Not only does it track your project's dependencies, it keeps both `requirements.txt` and `dev-requirements.txt` updated, enables autocompletion/intellisense for each dep, and allows you to import them just as you would on your device.\n\nThis allows you to include your requirement however you want, whether that be as a frozen module in your custom built firmware, or simply in the `/lib` folder on your device.\n\n#### Installing Packages\n\nTo add a package as a requirement for your project, run:\n\n`micropy install <PACKAGE_NAMES>`\n\nwhile in your project's root directory.\n\nThis will automatically execute the following:\n\n* Source `PACKAGE_NAMES` from pypi, as a url, or a local path\n* Retrieve the module/package and stub it, adding it to your local `.micropy` folder.\n* Add requirement to your `micropy.json`\n* Update `requirements.txt`\n\nTo install dev packages that are not needed on your device, but are needed for local development, add the `--dev` flag. This will do everything above **except** stub the requirement.\n\nYou can also install all requirements found in `micropy.json`/`requirements.txt`/`dev-requirements.txt` by executing `micropy install` without passing any packages. Micropy will automatically do this when setting up a local environment of an existing micropy project.\n\n#### Example\n\nLets say your new project will depend on [picoweb](https://pypi.org/project/picoweb/) and [blynklib](https://pypi.org/project/blynklib/). Plus, you'd like to use [rshell](https://pypi.org/project/rshell/) to communicate directly with your device. After creating your project via `micropy init`, you can install your requirements as so:\n\n<p align='center'>\n    <img width=\"70%\" src='https://github.com/BradenM/micropy-cli/raw/master/.github/img/install_demo.svg' alt=\"Micropy Pkg Install Demo\">\n</p>\n\nNow you or anybody cloning your project can import those requirements normally, and have the benefits of all the features micropy brings:\n\n<p align='center'>\n    <img width=\"70%\" src='https://github.com/BradenM/micropy-cli/raw/master/.github/img/deps_demo.gif' alt=\"Micropy Deps Demo\">\n</p>\n\n\n## Stub Management\n\nStub files are the magic behind how micropy allows features such as linting, Intellisense, and autocompletion to work. To achieve the best results with MicropyCli, its important that you first add the appropriate stubs for the device/firmware your project uses.\n\n> Note: When working in a micropy project, all stub related commands will also be executed on the active project. (i.e if in a project and you run `micropy stubs add <stub-name>`, then that stub retrieved AND added to the active project.)\n\n### Adding Stubs\n\nAdding stubs to Micropy is a breeze. Simply run: `micropy stubs add <STUB_NAME>`\nBy sourcing [micropy-stubs](https://github.com/BradenM/micropy-stubs), MicroPy has several premade stub packages to choose from.\n\nThese packages generally use the following naming schema:\n\n`<device>-<firmware>-<version>`\n\nFor example, running `micropy stubs add esp32-micropython-1.11.0` will install the following:\n* Micropython Specific Stubs\n* ESP32 Micropython v1.11 Device Specific Stubs\n* Frozen Modules for both device and firmware\n\nYou can search stubs that are made available to Micropy via `micropy stubs search <QUERY>`\n\nAlternatively, using `micropy stubs add <PATH>`, you can manually add stubs to Micropy.\nFor manual stub generation, please see [Josvel/micropython-stubber](https://github.com/Josverl/micropython-stubber).\n\n### Creating Stubs\n\nUsing `micropy stubs create <PORT/IP_ADDRESS>`, MicropyCli can automatically generate and add stubs from any Micropython device you have on hand. This can be done over both USB and WiFi.\n\n> Note: For stub creation, micropy-cli has additional dependencies.\n>\n> These can be installed by executing: `pip install micropy-cli[create_stubs]`\n\n\n### Viewing Stubs\n\nTo list stubs you have installed, simply run `micropy stubs list`.\n\nTo search for stubs for your device, use `micropy stubs search <QUERY>`.\n\n# See Also\n\n* [VSCode IntelliSense, Autocompletion & Linting capabilities](https://lemariva.com/blog/2019/08/micropython-vsc-ide-intellisense)\n    - An awesome article written by [lemariva](https://github.com/lemariva). It covers creating a micropython project environment from scratch using `micropy-cli` and [pymakr-vsc](pymakr-vsc). Great place to start if you're new to this!\n\n\n# Acknowledgements\n\n## Micropython-Stubber\n[Josvel/micropython-stubber](https://github.com/Josverl/micropython-stubber)\n\nJosverl's Repo is full of information regarding Micropython compatibility with VSCode and more. To find out more about how this process works, take a look at it.\n\nmicropy-cli and [micropy-stubs](https://github.com/BradenM/micropy-stubs) depend on micropython-stubber for its ability to generate frozen modules, create stubs on a pyboard, and more.\n"
  },
  {
    "path": "docs/cli.rst",
    "content": "CLI Usage\n=====\n\n.. click:: micropy.cli:cli\n   :prog: micropy\n\n.. click:: micropy.cli:init\n   :prog: micropy init\n   :show-nested:\n\n.. click:: micropy.cli:stubs\n   :prog: micropy stubs\n   :show-nested:\n\n.. click:: micropy.cli:install\n   :prog: micropy install\n   :show-nested:\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n\nimport os\nimport sys\n\nfrom recommonmark.transform import AutoStructify\n\ntry:\n    import importlib.metadata as importlib_metadata\nexcept ModuleNotFoundError:\n    import importlib_metadata\n\nsys.path.insert(0, os.path.abspath(\"..\"))\n\n\nsource_suffix = [\".rst\", \".md\"]\n\n# -- Project information -----------------------------------------------------\n\nproject = \"micropy-cli\"\ncopyright = \"2021, Braden Mars\"\nauthor = \"Braden Mars\"\n\ngithub_doc_root = \"https://github.com/BradenM/micropy-cli/tree/master/docs/\"\n\n# The full version, including alpha/beta/rc tags\nrelease = importlib_metadata.version(\"micropy-cli\")\n\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.coverage\",\n    \"sphinx.ext.napoleon\",\n    \"sphinx_autodoc_typehints\",\n    \"sphinx.ext.autosummary\",\n    \"sphinx.ext.autosectionlabel\",\n    \"sphinx_click.ext\",\n    \"recommonmark\",\n]\n\nautodoc_default_flags = [\"members\", \"show-inheritance\"]  # Defaults\nautosummary_generate = True  # Enable Autosummary\nautosummary_imported_members = True\nautosectionlabel_prefix_document = True\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = [\"_templates\"]\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\n\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = \"sphinx_rtd_theme\"\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = [\"_static\"]\n\n\n# At the bottom of conf.py\n\n\ndef setup(app):\n    app.add_config_value(\n        \"recommonmark_config\",\n        {\n            \"url_resolver\": lambda url: github_doc_root + url,\n            \"auto_toc_tree_section\": \"Contents\",\n            \"enable_eval_rst\": True,\n        },\n        True,\n    )\n    app.add_transform(AutoStructify)\n"
  },
  {
    "path": "docs/header.rst",
    "content": "Micropy Cli |PyPI| |PyPI - Python Version| |Github - Test Micropy Cli| |Coverage Status|\n========================================================================================\n\nMicropy Cli is a project management/generation tool for writing\n`Micropython`_ code in modern IDEs such as VSCode. Its primary goal is\nto automate the process of creating a workspace complete with:\n\n-  **Linting** compatible with Micropython\n-  VSCode **Intellisense**\n-  **Autocompletion**\n-  Dependency Management\n-  VCS Compatibility\n\n.. figure:: ../.github/img/micropy.svg\n   :width: 100%\n\n\nInstallation\n------------\n\nYou can download and install the latest version of this software from\nthe Python package index (PyPI) as follows:\n\n``pip install --upgrade micropy-cli``\n\n\n.. _Micropython: https://micropython.org/\n\n.. |PyPI| image:: https://img.shields.io/pypi/v/micropy-cli?logo=pypi&logoColor=white&style=flat-square\n   :target: https://pypi.org/project/micropy-cli/\n.. |PyPI - Python Version| image:: https://img.shields.io/pypi/pyversions/micropy-cli.svg?style=flat-square&logo=python&logoColor=green\n   :target: https://pypi.org/project/micropy-cli/\n.. |Github - Test Micropy Cli| image:: https://img.shields.io/github/workflow/status/BradenM/micropy-cli/Test%20MicropyCli/master?logo=github&style=flat-square\n   :target: https://github.com/BradenM/micropy-cli/actions\n.. |Coverage Status| image:: https://img.shields.io/coveralls/github/BradenM/micropy-cli/master?style=flat-square&logo=coveralls\n   :target: https://coveralls.io/github/BradenM/micropy-cli\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. include:: header.rst\n\n.. toctree::\n   :caption: Documentation\n   :maxdepth: 2\n\n   base.md\n   cli\n   modules\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs/modules.rst",
    "content": "API Reference\n=============\n\n.. autosummary::\n   :toctree: _autosummary\n\n   micropy\n   micropy.main\n   micropy.exceptions\n   micropy.stubs\n   micropy.stubs.source\n   micropy.project\n   micropy.project.modules\n   micropy.utils\n   micropy.config\n   micropy.config.config_source\n   micropy.packages\n"
  },
  {
    "path": "micropy/__init__.py",
    "content": "\"\"\"Micropy Cli.\n\nMicropy Cli is a project management/generation tool for writing Micropython\ncode in modern IDEs such as VSCode. Its primary goal is to automate the\nprocess of creating a workspace complete with:\n\nLinting compatible with Micropython,\nVSCode Intellisense,\nAutocompletion,\nDependency Management,\nVCS Compatibility\nand more.\n\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom micropy.main import MicroPy\nfrom micropy.utils._compat import metadata\n\n__version__ = metadata.version(\"micropy-cli\")\n"
  },
  {
    "path": "micropy/__main__.py",
    "content": "from __future__ import annotations\n\nimport sys\n\nif __name__ == \"__main__\":\n    from micropy.cli import cli\n\n    sys.exit(cli())\n"
  },
  {
    "path": "micropy/app/__init__.py",
    "content": "from .main import app\n\n__all__ = [\"app\"]\n"
  },
  {
    "path": "micropy/app/main.py",
    "content": "from __future__ import annotations\n\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import List, Optional, cast\n\nimport micropy.exceptions as exc\nimport questionary as prompt\nimport typer\nfrom micropy import logger, utils\nfrom micropy.main import MicroPy\nfrom micropy.project import Project, modules\nfrom micropy.stubs.stubs import Stub\nfrom micropy.utils._compat import metadata\nfrom questionary import Choice\n\nfrom .stubs import stubs_app\n\napp = typer.Typer(name=\"micropy-cli\", no_args_is_help=True, rich_markup_mode=\"markdown\")\napp.add_typer(stubs_app)\n\n\n@app.callback()\ndef main_callback(ctx: typer.Context):\n    \"\"\"\n    **Micropy CLI** is a project management/generation tool for writing [Micropython](https://micropython.org/) code in modern IDEs such as VSCode.\n\n    Its primary goal is to automate the process of creating a workspace complete with:\n\n    * **Linting** compatible with Micropython\n\n    * IDE **Intellisense**\n\n    * **Autocompletion**\n\n    * Dependency Management\n\n    * VCS Compatibility\n    \"\"\"\n    if ctx.resilient_parsing:\n        return\n    micropy = ctx.ensure_object(MicroPy)\n    if not micropy.project.exists:\n        return\n    latest = utils.is_update_available()\n    if latest:\n        log = logger.Log.get_logger(\"MicroPy\")\n        log.title(\"Update Available!\")\n        log.info(f\"Version $B[v{latest}] is now available\")\n        log.info(\"You can update via: $[pip install --upgrade micropy-cli]\\n\")\n\n\n@app.command(name=\"version\")\ndef main_version():\n    \"\"\"Print Micropy CLI Version.\"\"\"\n    vers = metadata.version(\"micropy-cli\")\n    print(f\"Micropy Version: {vers}\")\n    raise typer.Exit()\n\n\nTemplateEnum = Enum(\n    \"TemplateEnum\", {t: t for t in list(modules.TemplatesModule.TEMPLATES.keys())}, type=str\n)\n\n\ndef template_callback(\n    ctx: typer.Context, value: Optional[List[TemplateEnum]]\n) -> Optional[List[TemplateEnum]]:\n    if ctx.resilient_parsing:\n        return\n    if not value:\n        templates = modules.TemplatesModule.TEMPLATES.items()\n        templ_choices = [Choice(str(val[1]), value=t) for t, val in templates]\n        value = prompt.checkbox(\"Choose any Templates to Generate\", choices=templ_choices).ask()\n        if not value:\n            if not prompt.confirm(\n                \"You have chosen to use NO templates. Are you sure you want to continue?\",\n                default=False,\n            ).ask():\n                raise typer.Abort()\n            return []\n    value = [TemplateEnum(k) for k in value]\n    return value\n\n\ndef path_callback(ctx: typer.Context, value: Optional[Path]) -> Optional[Path]:\n    if ctx.resilient_parsing:\n        return\n    return value if value else Path.cwd()\n\n\ndef name_callback(ctx: typer.Context, value: Optional[str]) -> Optional[str]:\n    if ctx.resilient_parsing:\n        return\n    if not value:\n        path = ctx.params.get(\"path\", Path.cwd())\n        default_name = path.name\n        prompt_name = prompt.text(\"Project Name\", default=default_name).ask()\n        if prompt_name is None:\n            raise typer.Abort(\"You must provide a project name via prompt or --name option.\")\n        return prompt_name.strip()\n    return value\n\n\ndef stubs_callback(ctx: typer.Context, value: Optional[List[str]]) -> Optional[List[Stub]]:\n    if ctx.resilient_parsing:\n        return\n    mpy = ctx.ensure_object(MicroPy)\n    stub_values = (\n        [i for i in [mpy.stubs.add(s) for s in value] if i is not None]\n        if value\n        else list(mpy.stubs)\n    )\n    if not stub_values:\n        mpy.log.error(\"You don't have any stubs!\")\n        mpy.log.title(\"To add stubs to micropy, use $[micropy stubs add <STUB_NAME>]\")\n        mpy.log.info(\"See: $[micropy stubs --help] for more information.\")\n        raise typer.Abort(1)\n    if not value:\n        # if value was not explicitly provided, ask for selections.\n        stubs = [Choice(str(s), value=s) for s in stub_values]\n        stub_choices = prompt.checkbox(\"Which stubs would you like to use?\", choices=stubs).ask()\n        if not stub_choices:\n            # mpy.log.error(\"You must choose at least one stub!\")\n            raise typer.BadParameter(\n                \"You must choose at least one stub!\",\n                ctx,\n            )\n        return stub_choices\n    return stub_values\n\n\n@app.command(name=\"init\")\ndef main_init(\n    ctx: typer.Context,\n    path: Optional[Path] = typer.Argument(\n        None,\n        help=\"Path to project. Defaults to current working directory.\",\n        callback=path_callback,\n        dir_okay=True,\n        file_okay=False,\n        show_default=False,\n    ),\n    name: Optional[str] = typer.Option(\n        None,\n        \"--name\",\n        \"-n\",\n        help=\"Project Name. Defaults to path name.\",\n        show_default=False,\n        callback=name_callback,\n    ),\n    template: Optional[List[TemplateEnum]] = typer.Option(\n        None,\n        \"--template\",\n        \"-t\",\n        help=\"Templates to generate for project. Can be specified multiple times. Skips interactive prompt.\",\n        show_default=False,\n        callback=template_callback,\n    ),\n    stubs: Optional[List[str]] = typer.Option(\n        None,\n        \"--stubs\",\n        \"-s\",\n        help=\"Name of stubs to add to project. Can be specified multiple times. Skips interactive prompt.\",\n        callback=stubs_callback,\n        show_default=False,\n    ),\n):\n    \"\"\"Create new Micropython Project.\n\n    \\b When creating a new project, all files will be placed under the\n    generated <PROJECT_NAME> folder.\n\n    \"\"\"\n    mpy: MicroPy = ctx.find_object(MicroPy)\n    mpy.log.title(\"Creating New Project\")\n    # weird issue where \"template\" from args\n    # gets set a [None,None], but its correct in params.\n    template = ctx.params.get(\"template\", template)\n    project = Project(path, name=name)\n    project.add(modules.StubsModule, mpy.stubs, stubs=stubs)\n    project.add(modules.PackagesModule, \"requirements.txt\")\n    project.add(modules.DevPackagesModule, \"dev-requirements.txt\")\n    project.add(\n        modules.TemplatesModule,\n        templates=[t.value for t in template if t],\n        run_checks=mpy.RUN_CHECKS,\n    )\n    proj_path = project.create()\n    try:\n        rel_path = f\"./{proj_path.relative_to(Path.cwd())}\"\n    except ValueError:\n        rel_path = proj_path\n    mpy.log.title(f\"Created $w[{project.name}] at $w[{rel_path}]\")\n\n\ndef ensure_project(ctx: typer.Context) -> Project:\n    mpy = ctx.ensure_object(MicroPy)\n    project = mpy.project\n    if not project.exists:\n        mpy.log.error(\"You are not currently in an active project!\")\n        raise typer.Abort(1)\n    # todo: fix type issue.\n    return cast(Project, project)\n\n\ndef install_local_callback(ctx: typer.Context, value: Optional[Path]) -> Optional[Path]:\n    \"\"\"Handle package installation from local path.\"\"\"\n    if ctx.resilient_parsing:\n        return\n    if value is None:\n        return value\n    mpy = ctx.ensure_object(MicroPy)\n    project = ensure_project(ctx)\n    pkg_name = next(iter(ctx.args), None)\n    mpy.log.title(\"Installing Local Package\")\n    pkg_path = \"-e \" + str(value)\n    project.add_package(pkg_path, dev=ctx.params.get(\"dev\", False), name=pkg_name)\n    raise typer.Exit()\n\n\ndef install_project_callback(ctx: typer.Context, value: Optional[List[str]]) -> Optional[List[str]]:\n    \"\"\"Handle project requirements install.\"\"\"\n    if ctx.resilient_parsing:\n        return\n    if \"path\" in ctx.params:\n        return\n    if not value:\n        # only if no packages are provided.\n        mpy = ctx.ensure_object(MicroPy)\n        project = ensure_project(ctx)\n        mpy.log.title(\"Installing all Requirements\")\n        try:\n            project.add_from_file(dev=ctx.params.get(\"dev\", False))\n        except Exception as e:\n            mpy.log.error(\"Failed to load requirements!\", exception=e)\n            raise typer.Abort() from e\n        else:\n            mpy.log.success(\"\\nRequirements Installed!\")\n            raise typer.Exit()\n    return value\n\n\n@app.command(name=\"install\")\ndef main_install(\n    ctx: typer.Context,\n    packages: Optional[List[str]] = typer.Argument(\n        None, help=\"Packages to install.\", callback=install_project_callback\n    ),\n    dev: bool = typer.Option(\n        default=False,\n        help=\"Install as development package. This will not generate stubs for the package.\",\n        show_default=True,\n    ),\n    path: Optional[Path] = typer.Option(\n        None,\n        help=\"Add dependency from local path. Can be a file or directory.\",\n        callback=install_local_callback,\n    ),\n):\n    \"\"\"Install Packages as Project Requirements.\n\n    \\b\n    Install a project dependency while enabling\n    intellisense, autocompletion, and linting for it.\n\n        \\b\n        $ micropy install picoweb==1.8.2 blynklib\n        \\b\n\n\n\n    \\b\n    If no packages are passed and a requirements.txt file is found,\n    then micropy will install all packages listed in it.\n\n    \\b\n    If the --dev flag is passed, then the packages are only\n    added to micropy.json. They are not stubbed.\n\n    \\b\n    To add a dependency from a path, use the --path option\n    and provide a name for your package:\n\n        \\b\n        $ micropy install --path ./src/lib/mypackage MyCustomPackage\n        \\b\n\n\n\n    \\b\n    You can import installed packages just as you would\n    on your actual device:\n\n        \\b\n        _import <package_name>_\n\n    \"\"\"\n    mpy: MicroPy = ctx.ensure_object(MicroPy)\n    project = ensure_project(ctx)\n    mpy.log.title(\"Installing Packages\")\n    for pkg in packages:\n        try:\n            project.add_package(pkg, dev=dev)\n        except exc.RequirementException as e:\n            pkg_name = str(e.package)\n            mpy.log.error(f\"Failed to install {pkg_name}!\" \" Is it available on PyPi?\", exception=e)\n            raise typer.Abort() from e\n"
  },
  {
    "path": "micropy/app/stubs.py",
    "content": "from __future__ import annotations\n\nimport sys\nimport tempfile\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import List, Optional, Type\n\nimport micropy.exceptions as exc\nimport typer\nfrom micropy.exceptions import PyDeviceError\nfrom micropy.logger import Log\nfrom micropy.main import MicroPy\nfrom micropy.pyd import (\n    DevicePath,\n    MessageHandlers,\n    MetaPyDeviceBackend,\n    ProgressStreamConsumer,\n    PyDevice,\n)\nfrom micropy.pyd.backend_rshell import RShellPyDeviceBackend\nfrom micropy.pyd.backend_upydevice import UPyDeviceBackend\nfrom micropy.stubs import source as stubs_source\nfrom micropy.utils.stub import prepare_create_stubs\nfrom stubber.codemod import board as stub_board\nfrom stubber.codemod.modify_list import ListChangeSet\n\nstubs_app = typer.Typer(name=\"stubs\", rich_markup_mode=\"markdown\", no_args_is_help=True)\n\n\n@stubs_app.callback()\ndef stubs_callback():\n    \"\"\"Manage Micropy Stubs.\n\n    \\b\n    Stub files are what enable linting,\n    Intellisense, Autocompletion, and more.\n\n    \\b\n    To achieve the best results, you can install\n    stubs specific to your device/firmware using:\n\n     -  *micropy stubs search* `STUB_NAME`\n\n     -  *micropy stubs add* `STUB_NAME`\n\n\n\n    For more info, please check micropy stubs add --help\n    \"\"\"\n    pass\n\n\nclass CreateBackend(str, Enum):\n    upydevice = (\"upydevice\", UPyDeviceBackend)\n    rshell = (\"rshell\", RShellPyDeviceBackend)\n\n    def __new__(cls, value: str, backend: Type[MetaPyDeviceBackend]):\n        obj = str.__new__(cls, value)\n        obj._value_ = value\n        obj.backend = backend\n        return obj\n\n\ndef create_changeset(\n    value: Optional[List[str]], *, replace: bool = False\n) -> Optional[ListChangeSet]:\n    if value is None:\n        return value\n    return ListChangeSet.from_strings(add=value, replace=replace)\n\n\n@stubs_app.command(name=\"create\")\ndef stubs_create(\n    ctx: typer.Context,\n    port: str = typer.Argument(..., help=\"Serial port used to connect to device\"),\n    backend: CreateBackend = typer.Option(CreateBackend.upydevice, help=\"PyDevice backend to use.\"),\n    variant: stub_board.CreateStubsVariant = typer.Option(\n        stub_board.CreateStubsVariant.BASE,\n        \"-v\",\n        \"--variant\",\n        help=\"Create Stubs variant.\",\n        rich_help_panel=\"Stubs\",\n    ),\n    module: Optional[List[str]] = typer.Option(\n        None,\n        \"-m\",\n        \"--module\",\n        help=\"Modules to look for and stub. This flag can be used multiple times.\",\n        rich_help_panel=\"Stubs\",\n    ),\n    module_defaults: bool = typer.Option(\n        True, help=\"Include createstubs.py default modules.\", rich_help_panel=\"Stubs\"\n    ),\n    exclude: Optional[List[str]] = typer.Option(\n        None,\n        \"-e\",\n        \"--exclude\",\n        help=\"Modules to exclude from stubber. This flag can be used multiple times.\",\n        rich_help_panel=\"Stubs\",\n    ),\n    exclude_defaults: bool = typer.Option(\n        True,\n        help=\"Include createstubs.py default module excludes. This flag can be used multiple times.\",\n        rich_help_panel=\"Stubs\",\n    ),\n    compile: bool = typer.Option(\n        True,\n        \"-c\",\n        \"--compile\",\n        help=\"Cross compile to .mpy via mpy-cross.\",\n        rich_help_panel=\"Stubs\",\n    ),\n):\n    \"\"\"Create stubs from micropython-enabled devices.\n\n    Utilize Josverl's [micropython-stubber](https://github.com/josverl/micropython-stubber/)\n    to generate stubs from your own micropython-enabled device.\n\n    \\n\n    **Create stubs with defaults**:\\n\n     - `micropy stubs create /dev/ttyUSB0`\n\n    \\n\n    **Specify additional modules**:\\n\n     - `micropy stubs create -m custom_module -m other_module /dev/ttyUSB0`\\n\n     - _Only given modules_: `micropy stubs create -m custom_module --no-module-defaults /dev/ttyUSB0`\n\n    \\n\n    **Exclude additional modules**:\\n\n     - `micropy stubs create -e custom_module -e other_module /dev/ttyUSB0`\\n\n     - _Only exclude given modules_: `micropy stubs create -e custom_module --no-module-defaults /dev/ttyUSB0`\n\n    \\n\n    **Create Stubs Variants**:\\n\n     - **mem**: Optimized for low memory devices._\\n\n     - **db**: Persist stub progress across reboots.\\n\n     - **lvgl**: Additional support for LVGL devices.\\n\n\n    \"\"\"\n    mp: MicroPy = ctx.ensure_object(MicroPy)\n    log = mp.log\n    log.title(f\"Connecting to Pyboard @ $[{port}]\")\n    pyb_log = Log.add_logger(\"Pyboard\", \"bright_white\")\n\n    def _get_desc(name: str, cfg: dict):\n        desc = f\"{pyb_log.get_service()} {name}\"\n        return name, cfg | dict(desc=desc)\n\n    message_handler = MessageHandlers(\n        on_message=lambda x: isinstance(x, str) and pyb_log.info(x.strip())\n    )\n    try:\n        pyb = PyDevice(\n            port,\n            auto_connect=True,\n            stream_consumer=ProgressStreamConsumer(on_description=_get_desc),\n            message_consumer=message_handler,\n            backend=backend.backend,\n        )\n    except (SystemExit, PyDeviceError):\n        log.error(f\"Failed to connect, are you sure $[{port}] is correct?\")\n        return None\n\n    log.success(\"Connected!\")\n    if module or exclude:\n        log.title(\"Preparing createstubs for:\")\n        log.info(f\"Modules: {', '.join(module or [])}\")\n        log.info(f\"Exclude: {', '.join(exclude or [])}\")\n    create_stubs = prepare_create_stubs(\n        variant=variant,\n        modules_set=create_changeset(module, replace=not module_defaults),\n        exclude_set=create_changeset(exclude, replace=not exclude_defaults),\n        compile=compile,\n    )\n    dev_path = DevicePath(\"createstubs.mpy\") if compile else DevicePath(\"createstubs.py\")\n    log.info(\"Executing stubber on pyboard...\")\n    try:\n        pyb.run_script(create_stubs, DevicePath(dev_path))\n    except Exception as e:\n        # TODO: Handle more usage cases\n        log.error(f\"Failed to execute script: {str(e)}\", exception=e)\n        raise\n    log.success(\"Done!\")\n    log.info(\"Copying stubs...\")\n    with tempfile.TemporaryDirectory() as tmpdir:\n        pyb.copy_from(\n            DevicePath(\"/stubs\"),\n            tmpdir,\n            verify_integrity=True,\n            # exclude due to ps1 var possibly different.\n            exclude_integrity={\"sys.py\", \"usys.py\"},\n        )\n        out_dir = Path(tmpdir)\n        stub_path = next(out_dir.iterdir())\n        log.info(f\"Copied Stubs: $[{stub_path.name}]\")\n        stub_path = mp.stubs.from_stubber(stub_path, out_dir)\n        stub = mp.stubs.add(str(stub_path))\n    pyb.remove(dev_path)\n    pyb.disconnect()\n    log.success(f\"Added {stub.name} to stubs!\")\n    return stub\n\n\n@stubs_app.command(name=\"add\")\ndef stubs_add(ctx: typer.Context, stub_name: str, force: bool = False):\n    \"\"\"Add Stubs from package or path.\n\n    \\b\n    In general, stub package names follow this schema:\n        <device>-<firmware>-<version>\n\n    \\b\n    For example:\n        esp32-micropython-1.11.0\n\n    \\b\n    You can search premade stub packages using:\n        micropy stubs search <QUERY>\n\n    Checkout the docs on Github for more info.\n\n    \"\"\"\n    mpy: MicroPy = ctx.find_object(MicroPy)\n    proj = mpy.project\n    mpy.log.title(f\"Adding $[{stub_name}] to stubs\")\n    locator = stubs_source.StubSource(\n        [stubs_source.RepoStubLocator(mpy.repo), stubs_source.StubInfoSpecLocator()]\n    )\n    with locator.ready(stub_name) as stub:\n        stub_name = stub\n    try:\n        stub = mpy.stubs.add(stub_name, force=force)\n    except exc.StubNotFound:\n        mpy.log.error(f\"$[{stub_name}] could not be found!\")\n        sys.exit(1)\n    except exc.StubError:\n        mpy.log.error(f\"$[{stub_name}] is not a valid stub!\")\n        sys.exit(1)\n    else:\n        mpy.log.success(f\"{stub.name} added!\")\n        if proj.exists:\n            mpy.log.title(f\"Adding $[{stub.name}] to $[{proj.name}]\")\n            proj.add_stub(stub)\n\n\n@stubs_app.command(name=\"search\")\ndef stubs_search(ctx: typer.Context, query: str, show_outdated: bool = False):\n    \"\"\"Search available stubs.\"\"\"\n    mpy: MicroPy = ctx.find_object(MicroPy)\n    installed_stubs = map(str, mpy.stubs._loaded | mpy.stubs._firmware)\n    results = [\n        (r, r.name in installed_stubs)\n        for r in mpy.repo.search(query, include_versions=show_outdated)\n    ]\n    results = sorted(results, key=lambda pkg: pkg[0].name)\n    if not any(results):\n        mpy.log.warn(f\"No results found for: $[{query}].\")\n        sys.exit(0)\n    mpy.log.title(f\"Results for $[{query}]:\")\n    max_name = max(len(n[0].repo_name) for n in results)\n    for pkg, installed in results:\n        pad = max_name - len(pkg.repo_name) + 2\n        pad = pad if (pad % 2 == 0) else pad + 1\n        spacer = \"{:>{pad}}\".format(\"::\", pad=pad)\n        repo_logger = Log.add_logger(f\"{pkg.repo_name} {spacer}\", \"bright_white\")\n        name = \"{:>{pad}}\".format(f\"{pkg.name} ($w[{pkg.version}])\", pad=pad)\n        name = f\"{name} $B[(Installed)]\" if installed else name\n        repo_logger.info(name)\n\n\n@stubs_app.command(name=\"list\")\ndef stubs_list(ctx: typer.Context):\n    \"\"\"List installed stubs.\"\"\"\n    mpy: MicroPy = ctx.find_object(MicroPy)\n\n    def print_stubs(stub_list):\n        for firm, stubs in stub_list:\n            if stubs:\n                title = str(firm).capitalize()\n                mpy.log.title(f\"$[{title}]:\")\n                for stub in stubs:\n                    mpy.log.info(str(stub))\n\n    mpy.log.title(\"Installed Stubs:\")\n    mpy.log.info(f\"Total: {len(mpy.stubs)}\")\n    print_stubs(mpy.stubs.iter_by_firmware())\n    mpy.verbose = False\n    proj = mpy.project\n    if proj.exists:\n        mpy.log.title(f\"Stubs used in {proj.name}:\")\n        mpy.log.info(f\"Total: {len(proj.stubs)}\")\n        stubs = mpy.stubs.iter_by_firmware(stubs=proj.stubs)\n        print_stubs(stubs)\n"
  },
  {
    "path": "micropy/config/__init__.py",
    "content": "\"\"\"Configuration files and interfaces for them.\"\"\"\n\nfrom .config import Config\nfrom .config_dict import DictConfigSource\nfrom .config_json import JSONConfigSource\n\n__all__ = [\"Config\", \"JSONConfigSource\", \"DictConfigSource\"]\n"
  },
  {
    "path": "micropy/config/config.py",
    "content": "from copy import deepcopy\nfrom typing import Any, List, Optional, Sequence, Tuple, Type, Union\n\nimport dpath\nfrom boltons import iterutils\nfrom micropy.logger import Log, ServiceLog\n\nfrom .config_json import JSONConfigSource\nfrom .config_source import ConfigSource\n\n\"\"\"Config Interface\"\"\"\n\n\nclass Config:\n    \"\"\"Configuration File Interface.\n\n    Automatically syncs config in memory\n    with config saved to disk.\n\n    Args:\n        path (Path): Path to save file at.\n        source_format (ConfigSource, optional): Configuration File Format.\n            Defaults to JSONConfigSource.\n        default (dict, optional): Default configuration.\n                Defaults to {}.\n\n    \"\"\"\n\n    def __init__(\n        self,\n        *args: Any,\n        source_format: Type[ConfigSource] = JSONConfigSource,\n        default: Optional[dict] = None,\n    ):\n        default = default or dict()\n        self.log: ServiceLog = Log.add_logger(f\"{__name__}\")\n        self.format = source_format\n        self._source: ConfigSource = self.format(*args)\n        self._config = deepcopy(default)\n        if self._source.exists:\n            with self._source as src:\n                self.log.debug(\"loaded config values\")\n                dpath.util.merge(self._config, src, flags=dpath.MERGE_REPLACE)\n\n    @property\n    def source(self) -> ConfigSource:\n        return self._source\n\n    @source.setter\n    def source(self, value: Any) -> ConfigSource:\n        self._source = self.format(value)\n        return self._source\n\n    @property\n    def config(self) -> dict:\n        return self._config\n\n    def raw(self) -> dict:\n        return self._config\n\n    def sync(self) -> dict:\n        \"\"\"Sync in-memory config with disk.\n\n        Returns:\n            dict: updated config\n\n        \"\"\"\n        with self.source as src:\n            dpath.util.merge(src, self.config, flags=dpath.MERGE_REPLACE)\n        return self.config\n\n    def parse_key(self, key: str) -> Tuple[Sequence[str], str]:\n        \"\"\"Parses key.\n\n        Splits it into a path and 'final key'\n        object. Each key is separates by a: \"/\"\n\n        Example:\n            >>> self.parse_key('item/subitem/value')\n            (('item', 'subitem'), 'value')\n\n        Args:\n            key (str): key in dot notation\n\n        Returns:\n            Tuple[Sequence[str], str]: Parsed key\n\n        \"\"\"\n        full_path = tuple(i for i in key.split(\"/\"))\n        path = full_path[:-1]\n        p_key = full_path[-1]\n        return (path, p_key)\n\n    def get(self, key: str, default: Any = None) -> Any:\n        \"\"\"Retrieve config value.\n\n        Args:\n            key (str): Key (in dot-notation) of value to return.\n            default (Any, optional): Default value to return.\n                Defaults to None.\n\n        Returns:\n            Any: Value at key given\n\n        \"\"\"\n        try:\n            value = dpath.util.get(self.config, key)\n        except KeyError:\n            value = default\n        else:\n            return value\n        return value\n\n    def set(self, key: str, value: Any) -> Any:\n        \"\"\"Set config value.\n\n        Args:\n            key (str): Key (in dot-notation) to update.\n            value (Any): Value to set\n\n        Returns:\n            Any: Updated config\n\n        \"\"\"\n        dpath.set(self._config, key, value)\n        self.log.debug(f\"set config value [{key}] => {value}\")\n        return self.sync()\n\n    def add(self, key: str, value: Any) -> Any:\n        \"\"\"Overwrite or add config value.\n\n        Args:\n            key: Key to set\n            value: Value to add or update too\n\n        Returns:\n            Updated config\n\n        \"\"\"\n        dpath.new(self._config, key, value)\n        self.log.debug(f\"added config value [{key}] -> {value}\")\n        return self.sync()\n\n    def pop(self, key: str) -> Any:\n        \"\"\"Delete and return value at key.\n\n        Args:\n            key (str): Key to pop.\n\n        Returns:\n            Any: Popped value.\n\n        \"\"\"\n        path, target = self.parse_key(key)\n        value = self.get(key)\n        remapped = iterutils.remap(\n            self._config, lambda p, k, v: False if p == path and k == target else True\n        )\n        self._config = remapped\n        self.log.debug(f\"popped config value {value} <- [{key}]\")\n        return self.sync()\n\n    def extend(self, key: str, value: List[Any], unique: bool = False) -> dict:\n        \"\"\"Extend a list in config at key path.\n\n        Args:\n            key: Key to path to extend.\n            value: List of values to extend by.\n            unique: Only extend values if not already in values.\n\n        Returns:\n            Updated Config\n\n        \"\"\"\n        to_update = list(deepcopy(self.get(key, value)))\n        if unique:\n            value = [v for v in value if v not in to_update]\n        dpath.merge(to_update, value, flags=dpath.MERGE_ADDITIVE)\n        self.set(key, to_update)\n        return self.sync()\n\n    def upsert(self, key: str, value: Union[List[Any], dict]) -> dict:\n        \"\"\"Update or insert values into key list or dict.\n\n        Args:\n            key: Key to value to upsert.\n            value: Value to upsert by.\n\n        Returns:\n            Updated config.\n\n        \"\"\"\n        to_update = deepcopy(self.get(key, value))\n        dpath.merge(to_update, value, flags=dpath.MERGE_REPLACE)\n        self.add(key, to_update)\n        return self.sync()\n\n    def search(self, key):\n        \"\"\"Retrieve all values at key (with glob pattern).\n\n        Args:\n            key: Key with pattern to search with.\n\n        Returns:\n            Values matching key and pattern.\n\n        \"\"\"\n        return dpath.values(self.config, key)\n"
  },
  {
    "path": "micropy/config/config_dict.py",
    "content": "from typing import Optional\n\nfrom .config_source import ConfigSource\n\n\nclass DictConfigSource(ConfigSource):\n    def __init__(self, config: Optional[dict] = None):\n        \"\"\"Dict Config Source.\n\n        Args:\n            config (dict, optional): Initial Config.\n                Defaults to {}.\n\n        \"\"\"\n        super().__init__(initial_config=config)\n\n    @property\n    def exists(self) -> bool:\n        return any(self.config.keys())\n\n    def process(self) -> dict:\n        return self.config\n\n    def prepare(self):\n        return super().prepare()\n\n    def save(self, content: dict) -> dict:\n        return content\n"
  },
  {
    "path": "micropy/config/config_json.py",
    "content": "import json\nfrom pathlib import Path\n\nfrom boltons.fileutils import AtomicSaver\n\nfrom .config_source import ConfigSource\n\n\nclass JSONConfigSource(ConfigSource):\n    \"\"\"JSON Config File Source.\n\n    Args:\n        path (Path): Path to save config too.\n\n    \"\"\"\n\n    def __init__(self, path: Path):\n        super().__init__()\n        self._file_path: Path = path\n\n    @property\n    def file_path(self) -> Path:\n        \"\"\"Path to config file.\"\"\"\n        return self._file_path\n\n    @file_path.setter\n    def file_path(self, value: Path) -> Path:\n        \"\"\"Set config file path.\n\n        Args:\n            value (Path): New path to config file\n\n        Returns:\n            Path: Path to config file\n\n        \"\"\"\n        self._file_path = value\n        return self._file_path\n\n    @property\n    def exists(self) -> bool:\n        return self.file_path.exists()\n\n    def process(self) -> dict:\n        \"\"\"Load config from JSON file.\n\n        Returns:\n            dict: config in file\n\n        \"\"\"\n        content = self.file_path.read_text()\n        if not content:\n            return {}\n        config = json.loads(content)\n        return config\n\n    def prepare(self):\n        if not self.file_path.exists():\n            self.log.debug(f\"creating new config file: {self.file_path}\")\n            self.file_path.parent.mkdir(parents=True, exist_ok=True)\n            self.file_path.touch()\n\n    def save(self, content: dict) -> Path:\n        \"\"\"Save current config.\n\n        Args:\n            content (dict): content to write to file.\n\n        Returns:\n            Path: path to config file.\n\n        \"\"\"\n        config = json.dumps(content, indent=4, separators=(\",\", \": \"))\n        with AtomicSaver((str(self.file_path)), text_mode=True) as file:\n            file.write(config)\n        return self.file_path\n"
  },
  {
    "path": "micropy/config/config_source.py",
    "content": "\"\"\"Config Abstract.\"\"\"\n\nimport abc\nimport contextlib\nfrom typing import Any, Optional\n\nfrom micropy.logger import Log, ServiceLog\n\n\nclass ConfigSource(contextlib.AbstractContextManager, metaclass=abc.ABCMeta):\n    \"\"\"Abstract Base Class for Config Sources.\n\n    Args:\n        initial_config (dict, optional): Initial config values.\n            Defaults to {}.\n\n    \"\"\"\n\n    def __init__(self, initial_config: Optional[dict] = None):\n        self._config: dict = initial_config or dict()\n        self.log: ServiceLog = Log.add_logger(__name__)\n\n    @property\n    def config(self) -> dict:\n        \"\"\"Current Config Content.\"\"\"\n        return self._config\n\n    @config.setter\n    def config(self, value: dict) -> dict:\n        \"\"\"Set current config content.\n\n        Args:\n            value (dict): New value to set\n\n        Returns:\n            dict: Current config\n\n        \"\"\"\n        self._config = value\n        return self._config\n\n    @abc.abstractproperty\n    def exists(self) -> bool:\n        \"\"\"Property to check if source exists.\"\"\"\n\n    @abc.abstractmethod\n    def save(self, content: Any) -> Any:\n        \"\"\"Method to save config.\"\"\"\n\n    @abc.abstractmethod\n    def process(self) -> dict:\n        \"\"\"Read and process config file.\n\n        Returns:\n            dict: Config file content\n\n        \"\"\"\n\n    @abc.abstractmethod\n    def prepare(self) -> Any:\n        \"\"\"Method to prepare on enter.\"\"\"\n\n    def __enter__(self) -> dict:\n        self.prepare()\n        self._config = self.process()\n        return self._config\n\n    def __exit__(self, *args):\n        self.save(self._config)\n"
  },
  {
    "path": "micropy/data/__init__.py",
    "content": "\"\"\"\nmicropy.data\n~~~~~~~~~~~~~~\n\nThis module is merely to provide an easy method of locating\ndata files used by MicropyCli\n\"\"\"\n\nfrom pathlib import Path\n\n__all__ = [\"ROOT\", \"SCHEMAS\", \"REPO_SOURCES\", \"FILES\", \"STUB_DIR\", \"LOG_FILE\", \"STUBBER\"]\n\n# Paths\nMOD_PATH = Path(__file__).parent\nPATH = MOD_PATH.absolute()\nROOT = MOD_PATH.parent.absolute()\n\n# Stub Schemas\nSCHEMAS = PATH / \"schemas\"\n\n# Default Stub Sources\nREPO_SOURCES = PATH / \"sources.json\"\n\n# Application Data\nFILES = Path.home() / \".micropy\"\nSTUB_DIR = FILES / \"stubs\"\nLOG_FILE = FILES / \"micropy.log\"\n\n# Libraries\nLIB = ROOT / \"lib\"\nSTUBBER = LIB / \"stubber\"\n"
  },
  {
    "path": "micropy/data/schemas/firmware.json",
    "content": "{\n    \"type\": \"object\",\n    \"required\": [\n        \"name\",\n        \"repo\",\n        \"firmware\",\n        \"modules\",\n        \"devices\",\n        \"versions\"\n    ],\n    \"properties\": {\n        \"scope\": {\n            \"type\": \"string\"\n        },\n        \"name\": {\n            \"type\": \"string\"\n        },\n        \"repo\": {\n            \"type\": \"string\"\n        },\n        \"module_path\": {\n            \"type\": [\"string\", \"array\"]\n        },\n        \"firmware\": {\n            \"type\": \"string\"\n        },\n        \"excluded_modules\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"string\"\n            }\n        },\n        \"modules\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"string\"\n            }\n        },\n        \"devices\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"string\"\n            }\n        },\n        \"path\": {\n            \"type\": \"string\"\n        },\n        \"versions\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"required\": [\"version\", \"git_tag\", \"sha\", \"latest\", \"devices\"],\n                \"properties\": {\n                    \"version\": {\n                        \"type\": \"string\"\n                    },\n                    \"git_tag\": {\n                        \"type\": \"string\"\n                    },\n                    \"sha\": {\n                        \"type\": \"string\"\n                    },\n                    \"latest\": {\n                        \"type\": \"boolean\",\n                        \"default\": false\n                    },\n                    \"devices\": {\n                        \"type\": \"array\",\n                        \"items\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "micropy/data/schemas/stubs.json",
    "content": "{\n    \"type\": \"object\",\n    \"required\": [\"firmware\", \"modules\"],\n    \"properties\": {\n        \"firmware\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"nodename\",\n                \"version\",\n                \"sysname\"\n            ],\n            \"properties\": {\n                \"family\": {\n                    \"type\": \"string\"\n                },\n                \"machine\": {\n                    \"type\": \"string\"\n                },\n                \"firmware\": {\n                    \"type\": \"string\"\n                },\n                \"nodename\": {\n                    \"type\": \"string\"\n                },\n                \"version\": {\n                    \"type\": \"string\"\n                },\n                \"release\": {\n                    \"type\": \"string\"\n                },\n                \"sysname\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"stubber\": {\n            \"$id\": \"#/properties/stubber\",\n            \"type\": \"object\",\n            \"title\": \"The Stubber Schema\",\n            \"required\": [\"version\"],\n            \"properties\": {\n                \"version\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"modules\": {\n            \"$id\": \"#/properties/modules\",\n            \"type\": \"array\",\n            \"title\": \"The Modules Schema\",\n            \"items\": {\n                \"type\": \"object\",\n                \"required\": [\"file\", \"module\"],\n                \"properties\": {\n                    \"file\": {\n                        \"type\": \"string\"\n                    },\n                    \"module\": {\n                        \"type\": \"string\"\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "micropy/data/sources.json",
    "content": "[\n    {\n        \"display_name\": \"BradenM/micropy-stubs\",\n        \"name\": \"micropy-stubs\",\n        \"source\": \"https://raw.githubusercontent.com/BradenM/micropy-stubs/master/source.json\"\n    },\n    {\n        \"display_name\": \"Josverl/micropython-stubs\",\n        \"name\": \"micropython-stubs\",\n        \"source\": \"https://raw.githubusercontent.com/Josverl/micropython-stubs/main/publish/package_data.jsondb\"\n    }\n]\n"
  },
  {
    "path": "micropy/exceptions.py",
    "content": "\"\"\"Micropy Exceptions.\"\"\"\nfrom __future__ import annotations\n\n\nclass MicropyException(Exception):\n    \"\"\"Generic MicroPy Exception.\"\"\"\n\n\nclass StubError(MicropyException):\n    \"\"\"Exception for any errors raised by stubs.\"\"\"\n\n    def __init__(self, message=None, stub=None):\n        super().__init__(message)\n        self.stub = stub\n        self.message = message\n        if message is None:\n            message = \"An error occurred with this stub.\"\n\n\nclass StubValidationError(StubError):\n    \"\"\"Raised when a stub fails validation.\"\"\"\n\n    def __init__(self, path, errors, *args, **kwargs):\n        msg = f\"Stub at[{str(path)}] encountered\" f\" the following validation errors: {str(errors)}\"\n        super().__init__(msg, *args, **kwargs)\n\n    def __str__(self):\n        return self.message\n\n\nclass StubNotFound(StubError):\n    \"\"\"Raised when a stub cannot be found.\"\"\"\n\n    def __init__(self, stub_name=None):\n        stub_name = stub_name or \"Unknown\"\n        msg = f\"{stub_name} is not available!\"\n        super().__init__(msg)\n\n\nclass RequirementException(MicropyException):\n    \"\"\"A Requirement Exception Occurred.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        self.package = kwargs.pop(\"package\", None)\n        super().__init__(*args, **kwargs)\n\n\nclass RequirementNotFound(RequirementException):\n    \"\"\"A requirement could not be found.\"\"\"\n\n\nclass PyDeviceError(MicropyException):\n    \"\"\"Generic PyDevice exception.\"\"\"\n\n    def __init__(self, message: str = None):\n        super().__init__(message)\n        self.message = message\n\n\nclass PyDeviceConnectionError(PyDeviceError):\n    _default_message = \"Failed to connect to pydevice @ {location}.\"\n\n    def __init__(self, location: str):\n        super().__init__(self._default_message.format(location=location))\n\n\nclass PyDeviceFileIntegrityError(PyDeviceError):\n    _default_message = (\n        \"Failed to verify integrity: {device_path} (device={device_sum}, recv={digest})\"\n    )\n\n    def __init__(self, device_path: str, device_sum: str, digest: str):\n        super().__init__(\n            self._default_message.format(\n                device_path=device_path, device_sum=device_sum, digest=digest\n            )\n        )\n"
  },
  {
    "path": "micropy/logger.py",
    "content": "\"\"\"Logging functionality.\n\nTODO: Split logging from UI, refactor.\n\n\"\"\"\n\nimport logging\nimport re\nfrom contextlib import contextmanager\nfrom logging.handlers import RotatingFileHandler\n\nimport click\nfrom micropy import data\n\n\nclass Log:\n    \"\"\"Borg for easy access to any Log from anywhere in the package.\"\"\"\n\n    __shared_state = {}\n\n    def __init__(self):\n        self.__dict__ = self.__shared_state\n        self.parent_logger = ServiceLog()\n        self.loggers = [self.parent_logger]\n\n    @classmethod\n    def add_logger(cls, service_name, base_color=\"white\", **kwargs):\n        \"\"\"Creates a new child ServiceLog instance.\"\"\"\n        _self = cls()\n        parent = kwargs.pop(\"parent\", _self.parent_logger)\n        logger = ServiceLog(service_name, base_color, parent=parent, **kwargs)\n        _self.loggers.append(logger)\n        return logger\n\n    @classmethod\n    def get_logger(cls, service_name):\n        \"\"\"Retrieves a child logger by service name.\"\"\"\n        _self = cls()\n        logger = next(i for i in _self.loggers if i.service_name == service_name)\n        return logger\n\n\nclass ServiceLog:\n    \"\"\"Handles logging to stdout and micropy.log.\n\n    :param service_name: Active service to display\n    :type service_name: str\n    :param base_color: Color of name on output\n    :type base_color: str\n\n    \"\"\"\n\n    LOG_FILE = data.LOG_FILE\n\n    def __init__(self, service_name=\"MicroPy\", base_color=\"bright_green\", **kwargs):\n        self.parent = kwargs.get(\"parent\", None)\n        self.LOG_FILE.parent.mkdir(exist_ok=True)\n        self.base_color = base_color\n        self.service_name = service_name\n        self.load_handler()\n        self.info_color = kwargs.get(\"info_color\", \"white\")\n        self.accent_color = kwargs.get(\"accent_color\", \"yellow\")\n        self.warn_color = kwargs.get(\"warn_color\", \"green\")\n        self.show_title = kwargs.get(\"show_title\", True)\n        self.stdout = kwargs.get(\"stdout\", True)\n\n    @contextmanager\n    def silent(self):\n        self.stdout = False\n        yield self\n        self.stdout = True\n\n    def load_handler(self):\n        \"\"\"Loads Logging Module Formatting.\"\"\"\n        self.log = logging.getLogger()\n        if not self.log.hasHandlers():\n            self.log.setLevel(logging.DEBUG)\n            self.log_handler = RotatingFileHandler(\n                str(self.LOG_FILE),\n                mode=\"a\",\n                maxBytes=2 * 1024 * 1024,\n                backupCount=2,\n                encoding=None,\n                delay=0,\n            )\n            self.log_handler.setLevel(logging.DEBUG)\n            self.log.addHandler(self.log_handler)\n        self.log_handler = self.log.handlers[0]\n        log_format = \"[%(asctime)s] %(levelname)s: \" f\"{self.service_name.lower()}: \" \"%(message)s\"\n        self.log_handler.setFormatter(logging.Formatter(log_format, \"%Y-%m-%d %H:%M:%S\"))\n\n    def parse_msg(self, msg, accent_color=None):\n        \"\"\"Parses any color codes accordingly.\n\n        :param str msg:\n        :param str accent_color:  (Default value = None)\n        :return: Parsed Message\n        :rtype: str\n\n        \"\"\"\n        msg_special = re.findall(r\"\\$(.*?)\\[(.*?)\\]\", msg)\n        color = accent_color or self.accent_color\n        special = {\"fg\": color, \"bold\": True}\n        clean = msg\n        _parts = re.split(r\"\\$.*?\\[(.*?)\\]\", msg)\n        parts = [(p, None) for p in _parts]\n        for w in msg_special:\n            if w[0] == \"w\":\n                special[\"fg\"] = self.warn_color\n            if w[0] == \"B\":\n                special.pop(\"fg\")\n            sindex = _parts.index(w[1])\n            parts[sindex] = (w[1], special)\n            clean = msg.replace(f\"${w[0]}[{w[1]}]\", w[1])\n        clean = clean.encode(\"ascii\", \"ignore\").decode(\"utf-8\").strip()\n        return (parts, clean)\n\n    def get_parents(self, names=None):\n        \"\"\"Retrieve all parents.\"\"\"\n        names = names or []\n        if len(names) == 0:\n            names = [self.service_name]\n        if self.parent:\n            names.insert(0, self.parent.service_name)\n            names = self.parent.get_parents(names)\n        return names\n\n    def get_service(self, **kwargs):\n        \"\"\"Retrieves formatted service title.\n\n        :param **kwargs:\n        :return: formatted title\n        :rtype: str\n\n        \"\"\"\n        if not self.show_title:\n            return f\"{self.parent.get_service(bold=True)}\"\n        color = kwargs.pop(\"fg\", self.base_color)\n        title = click.style(f\"{self.service_name}\", fg=color, **kwargs)\n        title = f\"{title}{click.style(' ', fg=color)}\"\n        if self.parent is not None:\n            title = f\"{self.parent.get_service(bold=True)} {title}\"\n        return title\n\n    def iter_formatted(self, message, **kwargs):\n        \"\"\"Iterate formatted message tuple into styled string.\n\n        Args:\n            message (tuple): tuple as (msg, style)\n\n        \"\"\"\n        if isinstance(message, str):\n            message, _ = self.parse_msg(message)\n        for msg in message:\n            text, mstyle = msg\n            mstyle = mstyle or kwargs\n            yield click.style(text, **mstyle)\n\n    def echo(self, msg, **kwargs):\n        \"\"\"Prints msg to stdout.\n\n        :param str msg: message to print\n        :param **kwargs:\n\n        \"\"\"\n        title_color = kwargs.pop(\"title_color\", self.base_color)\n        title_bold = kwargs.pop(\"title_bold\", True)\n        accent_color = kwargs.pop(\"accent\", self.accent_color)\n        service_title = self.get_service(fg=title_color, bold=title_bold)\n        message, clean = self.parse_msg(msg, accent_color)\n        log_attr = kwargs.pop(\"log\", None)\n        if log_attr:\n            self.load_handler()\n            log_func = getattr(logging, log_attr)\n            log_func(clean)\n        if self.stdout:\n            init_msg, init_style = message[0]\n            first_part, nl_part, _ = init_msg.partition(\"\\n\")\n            fp_clean = first_part.encode(\"ascii\", \"ignore\").decode(\"unicode_escape\")\n            if not fp_clean.strip() and nl_part == \"\\n\":\n                init_msg = init_msg.replace(\"\\n\", \"\")\n                message[0] = (init_msg, init_style)\n                click.secho(\"\")\n            click.secho(f\"{service_title} \", nl=False)\n            post_nl = kwargs.pop(\"nl\", None)\n            formatted = list(self.iter_formatted(message, **kwargs))\n            for msg in formatted:\n                do_nl = msg == formatted[-1]\n                click.echo(msg, nl=do_nl)\n            if post_nl:\n                click.echo(\"\")\n\n    def info(self, msg, **kwargs):\n        \"\"\"Prints message with info formatting.\n\n        :param msg:\n        :param **kwargs:\n        :return: method to print msg\n        :rtype: method\n\n        \"\"\"\n        return self.echo(msg, log=\"info\", **kwargs)\n\n    def title(self, msg, **kwargs):\n        \"\"\"Prints bolded info message.\n\n        Args:\n            msg (str): Message\n\n        \"\"\"\n        return self.info(f\"\\n{msg}\", bold=True)\n\n    def error(self, msg, exception=None, **kwargs):\n        \"\"\"Prints message with error formatting.\n\n        :param msg:\n        :param **kwargs:\n        :return: method to print msg\n        :rtype: method\n\n        \"\"\"\n        bold = kwargs.pop(\"bold\", (exception is not None))\n        self.echo(\n            msg,\n            log=\"error\",\n            title_color=\"red\",\n            title_bold=True,\n            fg=\"red\",\n            accent=\"red\",\n            bold=bold,\n            **kwargs,\n        )\n        if exception:\n            return self.exception(exception)\n\n    def warn(self, msg, **kwargs):\n        \"\"\"Prints message with warn formatting.\n\n        :param msg:\n        :param **kwargs:\n        :return: method to print msg\n        :rtype: method\n\n        \"\"\"\n        return self.echo(msg, log=\"warning\", title_color=\"red\", title_bold=True)\n\n    def exception(self, error, **kwargs):\n        \"\"\"Prints message with exception formatting.\n\n        :param error:\n        :param **kwargs:\n        :return: method to print msg\n        :rtype: method\n\n        \"\"\"\n        name = type(error).__name__\n        msg = f\"{name}: {str(error)}\"\n        return self.echo(msg, log=\"exception\", title_color=\"red\", fg=\"red\", accent=\"red\", **kwargs)\n\n    def success(self, msg, **kwargs):\n        \"\"\"Prints message with success formatting.\n\n        :param msg:\n        :param **kwargs:\n        :return: method to print msg\n        :rtype: method\n        :return: method to print msg\n        :rtype: method\n\n        \"\"\"\n        message = f\"\\u2714 {msg}\"\n        return self.echo(message, log=\"info\", fg=\"green\", **kwargs)\n\n    def debug(self, msg, **kwargs):\n        \"\"\"Prints message with debug formatting.\n\n        :param msg:\n        :param **kwargs:\n        :return: method to log msg\n        :rtype: method\n\n        \"\"\"\n        if self.stdout:\n            with self.silent():\n                return self.debug(msg, **kwargs)\n        self.echo(msg, log=\"debug\")\n        return msg\n"
  },
  {
    "path": "micropy/main.py",
    "content": "\"\"\"Main Module.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import List, Optional\n\nimport attr\nfrom micropy import data, utils\nfrom micropy.logger import Log\nfrom micropy.project import Project, modules\nfrom micropy.stubs import RepositoryInfo, StubManager, StubRepository\nfrom pydantic import parse_file_as\n\n\n@attr.define(kw_only=True)\nclass MicroPyOptions:\n    root_dir: Path = attr.field(default=data.FILES)\n    stubs_dir: Path = attr.Factory(lambda self: self.root_dir / \"stubs\", takes_self=True)\n\n\nclass MicroPy:\n    \"\"\"Handles App State Management.\"\"\"\n\n    RUN_CHECKS = True\n    repo: StubRepository\n    config: MicroPyOptions\n    _stubs: Optional[StubManager] = None\n\n    def __init__(self, *, options: Optional[MicroPyOptions] = None):\n        self.config = options or MicroPyOptions()\n        self.log = Log.get_logger(\"MicroPy\")\n        self.verbose = True\n        self.log.debug(\"MicroPy Loaded\")\n        repo_list = parse_file_as(List[RepositoryInfo], data.REPO_SOURCES)\n        self.repo = StubRepository()\n        for repo_info in repo_list:\n            self.repo = self.repo.add_repository(repo_info)\n        if not self.config.stubs_dir.exists():\n            self.setup()\n\n    def setup(self):\n        \"\"\"Creates necessary directories for micropy.\"\"\"\n        self.log.debug(\"Running first time setup...\")\n        self.log.debug(f\"Creating .micropy directory @ {self.config.root_dir}\")\n        self.config.stubs_dir.mkdir(parents=True, exist_ok=True)\n\n    @property\n    def stubs(self) -> StubManager:\n        \"\"\"Primary Stub Manager for MicroPy.\n\n        Returns:\n            StubManager: StubManager Instance\n\n        \"\"\"\n        if not self._stubs:\n            self._stubs = StubManager(resource=self.config.stubs_dir, repos=self.repo)\n        return self._stubs\n\n    @utils.lazy_property\n    def project(self):\n        \"\"\"Current active project if available.\n\n        Returns:\n            Project: Instance of Current Project\n\n        \"\"\"\n        proj = self.resolve_project(\".\", verbose=self.verbose)\n        return proj\n\n    def resolve_project(self, path, verbose=True):\n        \"\"\"Returns project from path if it exists.\n\n        Args:\n            path (str): Path to test\n            verbose (bool): Log to stdout. Defaults to True.\n\n        Returns:\n            Project if it exists\n\n        \"\"\"\n        path = Path(path).absolute()\n        proj = Project(path)\n        proj.add(modules.StubsModule, self.stubs)\n        proj.add(modules.PackagesModule, \"requirements.txt\")\n        proj.add(modules.DevPackagesModule, \"dev-requirements.txt\")\n        proj.add(modules.TemplatesModule, run_checks=self.RUN_CHECKS)\n        if proj.exists:\n            if verbose:\n                self.log.title(\"Loading Project\")\n            proj.load()\n            if verbose:\n                self.log.success(\"Ready!\")\n            return proj\n        return proj\n"
  },
  {
    "path": "micropy/packages/__init__.py",
    "content": "\"\"\"Packages Module.\n\nAllows user to address different dependency types (package, module,\npath, pypi, etc.) through a single uniform api.\n\n\"\"\"\n\nfrom pathlib import Path\nfrom typing import Any, Optional, Union\n\nimport requirements\n\nfrom .package import Package\nfrom .source_package import PackageDependencySource, VCSDependencySource\nfrom .source_path import LocalDependencySource\n\n\ndef create_dependency_source(\n    requirement: str, name: Optional[str] = None, **kwargs: Any\n) -> Union[LocalDependencySource, PackageDependencySource, VCSDependencySource]:\n    \"\"\"Factory for creating a dependency source object.\n\n    Args:\n        requirement (str): Package name/path/constraints in string form.\n        name (str, optional): Override package name.\n            Defaults to None.\n\n    Returns:\n        Appropriate Dependency Source\n\n    \"\"\"\n    req = next(requirements.parse(str(requirement)))\n    if req.local_file:\n        path = Path(req.path)\n        name = name or path.name\n        pkg = Package(name, req.specs, path=req.path)\n        source = LocalDependencySource(pkg, path)\n        return source\n    pkg = Package(**req.__dict__)\n    if pkg.vcs is not None or pkg.revision is not None:\n        return VCSDependencySource(pkg, **kwargs)\n    return PackageDependencySource(pkg, **kwargs)\n\n\n__all__ = [\n    \"Package\",\n    \"PackageDependencySource\",\n    \"LocalDependencySource\",\n    \"create_dependency_source\",\n]\n"
  },
  {
    "path": "micropy/packages/package.py",
    "content": "from pathlib import Path\nfrom typing import List, Optional, Tuple\n\nimport requirements\nfrom packaging.utils import canonicalize_name\n\n\nclass Package:\n    def __init__(\n        self,\n        name: str,\n        specs: List[Tuple[str, str]],\n        path: Optional[Path] = None,\n        uri: Optional[str] = None,\n        vcs: Optional[str] = None,\n        revision: Optional[str] = None,\n        line: Optional[str] = None,\n        **kwargs,\n    ):\n        \"\"\"Generic Python Dependency.\n\n        Args:\n            name (str): Name of package\n            specs (List[Tuple[str, str]]): Package constraints.\n            path: path to package\n\n        \"\"\"\n        self._name = name\n        self._specs = specs\n        self._path = path\n        self._uri = uri\n        self._vcs = vcs\n        self._revision = revision\n        self._line = line\n        self.editable = self._path is not None\n\n    @property\n    def name(self) -> str:\n        return canonicalize_name(self._name)\n\n    @property\n    def path(self) -> Optional[Path]:\n        if not self._path:\n            return None\n        return Path(self._path)\n\n    @property\n    def full_name(self) -> str:\n        if self._line and self._vcs:\n            return self._line\n        if self._path:\n            return self.pretty_specs\n        if not self._specs:\n            return self.name\n        return f\"{self.name}{self.pretty_specs}\"\n\n    @property\n    def uri(self) -> Optional[str]:\n        if self._vcs and self._vcs in self._uri[:4]:\n            # handle 'git+https' schemas\n            return self._uri[4:]\n        return self._uri\n\n    @property\n    def vcs(self) -> Optional[str]:\n        return self._vcs\n\n    @property\n    def revision(self) -> Optional[str]:\n        return self._revision\n\n    @property\n    def line(self) -> Optional[str]:\n        return self._line\n\n    @property\n    def specs(self) -> List[Tuple[str, str]]:\n        return self._specs\n\n    @property\n    def pretty_specs(self) -> str:\n        if self.line and self.vcs:\n            return self.line\n        if self._path:\n            return f\"-e {self._path}\"\n        if not self.specs:\n            return \"*\"\n        _specs = [\"\".join(i for i in s) for s in self.specs]\n        return \"\".join(_specs)\n\n    @classmethod\n    def from_text(cls, name: str, specs: str) -> \"Package\":\n        \"\"\"Create package from text.\n\n        Args:\n            name: name of package\n            specs: package constraints\n\n        Returns:\n            Package instance\n\n        \"\"\"\n        if \"http\" in specs:\n            req = next(requirements.parse(specs))\n            return cls(**req.__dict__)\n        if \"-e\" in specs:\n            req = next(requirements.parse(specs))\n            return cls(name, req.specs, path=req.path)\n        req_name = name\n        if specs != \"*\":\n            req_name = f\"{name}{specs}\"\n        req = next(requirements.parse(req_name))\n        return cls(req.name, req.specs)\n\n    def __str__(self) -> str:\n        return self.full_name\n"
  },
  {
    "path": "micropy/packages/source.py",
    "content": "from contextlib import AbstractContextManager, ExitStack, contextmanager\nfrom pathlib import Path\nfrom typing import List, Optional, Tuple\n\nfrom boltons import fileutils\nfrom micropy import utils\nfrom micropy.logger import Log, ServiceLog\n\nfrom .package import Package\n\n\nclass DependencySource(AbstractContextManager):\n    \"\"\"Base class for managing dependency sources.\n\n    Args:\n        package (Package): package the source points too.\n\n    \"\"\"\n\n    _ignore_stubs: List[str] = [\"setup.py\", \"__version__\", \"test_\"]\n\n    def __init__(self, package: Package):\n        self.is_local = False\n        self._package = package\n        self.log: ServiceLog = Log.add_logger(repr(self))\n\n    @property\n    def package(self) -> Package:\n        return self._package\n\n    @contextmanager\n    def handle_cleanup(self):\n        with ExitStack() as stack:\n            stack.push(self)\n            yield\n            # no errors, continue on\n            stack.pop_all()\n\n    def get_root(self, path: Path) -> Optional[Path]:\n        \"\"\"Determines package root if it has one.\n\n        Args:\n            path (Path): Path to check\n\n        Returns:\n            bool: True if is package\n\n        \"\"\"\n        init = next(path.rglob(\"__init__.py\"), None)\n        if init:\n            return init.parent\n        return None\n\n    def generate_stubs(self, path: Path) -> List[Tuple[Path, Path]]:\n        \"\"\"Generate Stub Files from a package.\n\n        Args:\n            path (Path): Path to package.\n\n        Returns:\n            List[Tuple[Path, Path]]: List of tuples containing\n                 a path to the original file and stub, respectively.\n\n        \"\"\"\n        py_files = fileutils.iter_find_files(str(path), patterns=\"*.py\", ignored=self._ignore_stubs)\n        stubs = [utils.generate_stub(f) for f in py_files]\n        return stubs\n\n    def __enter__(self):\n        \"\"\"Method to prepare source.\"\"\"\n\n    def __exit__(self, *args):\n        return super().__exit__(*args)\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__} {self.package}>\"\n"
  },
  {
    "path": "micropy/packages/source_package.py",
    "content": "import shutil\nfrom pathlib import Path\nfrom tempfile import mkdtemp\nfrom typing import Any, Callable, List, Optional, Tuple, Union\n\nfrom git import Repo\nfrom micropy import utils\nfrom micropy.exceptions import RequirementNotFound\n\nfrom .package import Package\nfrom .source import DependencySource\n\n\nclass PackageDependencySource(DependencySource):\n    \"\"\"Dependency Source for pypi packages.\n\n    Args:\n        package (Package): Package source points too.\n        format_desc: Callback to format progress bar description.\n            Defaults to None.\n\n    \"\"\"\n\n    repo: str = \"https://pypi.org/pypi/{name}/json\"\n\n    def __init__(self, package: Package, format_desc: Optional[Callable[..., Any]] = None):\n        super().__init__(package)\n        try:\n            utils.ensure_valid_url(self.repo_url)\n        except Exception as e:\n            raise RequirementNotFound(\n                f\"{self.repo_url} is not a valid url!\", package=self.package\n            ) from e\n        else:\n            self._meta: dict = utils.get_package_meta(str(self.package), self.repo_url)\n            self.format_desc = format_desc or (lambda n: n)\n\n    @property\n    def repo_url(self) -> str:\n        _url = self.repo.format(name=self.package.name)\n        return _url\n\n    @property\n    def source_url(self) -> str:\n        return self._meta.get(\"url\", None)\n\n    @property\n    def file_name(self) -> str:\n        return utils.get_url_filename(self.source_url)\n\n    def fetch(self) -> bytes:\n        \"\"\"Fetch package contents into memory.\n\n        Returns:\n            bytes: Package archive contents.\n\n        \"\"\"\n        self.log.debug(f\"fetching package: {self.file_name}\")\n        desc = self.format_desc(self.file_name)\n        content = utils.stream_download(self.source_url, desc=desc)\n        return content\n\n    def __enter__(self) -> Union[Path, List[Tuple[Path, Path]]]:\n        \"\"\"Prepare Pypi package for installation.\n\n        Extracts the package into a temporary directory then\n        generates stubs for type hinting.\n        This helps with intellisense.\n\n        If the dependency is a module, a list\n        of tuples with the file and stub path, respectively,\n        will be returned. Otherwise, the path to the package\n        root will be returned.\n\n        Returns:\n            Root package path or list of files.\n\n        \"\"\"\n        self.tmp_path = Path(mkdtemp())\n        with self.handle_cleanup():\n            path = utils.extract_tarbytes(self.fetch(), self.tmp_path)\n            stubs = self.generate_stubs(path)\n            pkg_root = self.get_root(path)\n        return pkg_root or stubs\n\n    def __exit__(self, *args):\n        shutil.rmtree(self.tmp_path, ignore_errors=True)\n        return super().__exit__(*args)\n\n\nclass VCSDependencySource(DependencySource):\n    \"\"\"Dependency Source for vcs packages.\"\"\"\n\n    def __init__(self, package: Package, format_desc: Optional[Callable[..., Any]] = None):\n        super().__init__(package)\n        self.log.debug(\n            f\"VCS package!, {self.package.revision}@{self.package.vcs}@{self.package.full_name}\"\n        )\n        self._repo: Optional[Repo] = None\n        try:\n            utils.ensure_valid_url(self.repo_url)\n        except Exception as e:\n            raise RequirementNotFound(\n                f\"{self.repo_url} is not a valid VCS url!\", package=self.package\n            ) from e\n        else:\n            self.format_desc = format_desc or (lambda n: n)\n\n    @property\n    def repo_url(self) -> str:\n        return self.package.uri\n\n    @property\n    def source_url(self) -> str:\n        return self.package.uri\n\n    @property\n    def file_name(self) -> str:\n        return self.package.name\n\n    def fetch(self, dest_path: Path) -> Path:\n        \"\"\"Clones VCS repository to a given directory.\n\n        Args:\n            dest_path: Path to clone directory too.\n\n        Returns:\n            Path to clone repository.\n\n        \"\"\"\n        self.log.debug(f\"fetching vcs package: ${self.file_name} @ ${self.repo_url}\")\n        self.format_desc(self.file_name)\n        self._repo = Repo.clone_from(self.repo_url, str(dest_path))\n        return dest_path\n\n    def __enter__(self) -> Union[Path, List[Tuple[Path, Path]]]:\n        \"\"\"Prepare VCS repository for installation.\n\n        See PackageDependencySource.__enter__\n\n        Returns:\n            Root package path or list of files.\n\n        \"\"\"\n        self.tmp_path = Path(mkdtemp())\n        with self.handle_cleanup():\n            path = self.fetch(self.tmp_path)\n            stubs = self.generate_stubs(path)\n            pkg_root = self.get_root(path)\n        return pkg_root or stubs\n\n    def __exit__(self, *args):\n        shutil.rmtree(self.tmp_path, ignore_errors=True)\n        return super().__exit__(*args)\n"
  },
  {
    "path": "micropy/packages/source_path.py",
    "content": "from pathlib import Path\nfrom typing import List, Optional, Tuple, Union\n\nfrom .package import Package\nfrom .source import DependencySource\n\n\nclass LocalDependencySource(DependencySource):\n    \"\"\"Dependency Source that is available locally.\n\n    Args:\n        package (Package): Package source points too.\n        path (Path): Path to package.\n\n    \"\"\"\n\n    def __init__(self, package: Package, path: Path):\n        super().__init__(package)\n        self._path = path\n        self.is_local = True\n\n    @property\n    def path(self) -> Path:\n        return self._path\n\n    def __enter__(self) -> Union[Path, List[Tuple[Path, Optional[Path]]]]:\n        \"\"\"Determines appropriate path.\n\n        Returns:\n            Path to package root or list of files.\n\n        \"\"\"\n        return self.path\n"
  },
  {
    "path": "micropy/project/__init__.py",
    "content": "\"\"\"Module for generating/managing projects.\"\"\"\n\nfrom . import modules\nfrom .project import Project\n\n__all__ = [\"Project\", \"modules\"]\n"
  },
  {
    "path": "micropy/project/checks.py",
    "content": "\"\"\"Various requirement checks for templates.\"\"\"\n\nimport subprocess as subproc\nfrom functools import partial as _p\n\nfrom micropy.logger import Log\nfrom packaging import version\n\nVSCODE_MS_PY_MINVER = \"2019.9.34474\"\n\nlog = Log.get_logger(\"MicroPy\")\n\n\ndef iter_vscode_ext(name=None):\n    \"\"\"Iterates over installed VSCode Extensions.\n\n    Args:\n        name (str, optional): Name of Extension to Yield\n\n    \"\"\"\n    _cmd = \"code --list-extensions --show-versions\"\n    proc = subproc.run(_cmd, capture_output=True, shell=True)\n    results = [e.strip() for e in proc.stdout.splitlines()]\n    for ext in results:\n        ename, vers = ext.split(\"@\")\n        if not name:\n            yield (ename, vers)\n        if name and ename == name:\n            yield (ename, vers)\n\n\ndef vscode_ext_min_version(ext, min_version=VSCODE_MS_PY_MINVER, info=None):\n    \"\"\"Check if installed VScode Extension meets requirements.\n\n    Args:\n        ext (str): Name of Extension to Test\n        min_version (str, optional): Minimum version.\n            Defaults to VSCODE_MS_PY_MINVER.\n        info (str, optional): Additional information to output.\n            Defaults to None.\n\n    Returns:\n        bool: True if requirement is satisfied, False otherwise.\n\n    \"\"\"\n    try:\n        name, vers = next(iter_vscode_ext(name=ext), (ext, \"0.0.0\"))\n    except Exception as e:\n        log.debug(f\"vscode check failed to run: {e}\")\n        log.debug(\"skipping...\")\n        return True\n    else:\n        cur_vers = version.parse(vers)\n        min_vers = version.parse(min_version)\n        if cur_vers >= min_vers:\n            return True\n        log.error(f\"\\nVSCode Extension {ext} failed to satisfy requirements!\", bold=True)\n        log.error(f\"$[Min Required Version]: {min_vers}\")\n        log.error(f\"$[Current Version:] {cur_vers}\")\n        if info:\n            log.warn(info)\n        return False\n\n\nTEMPLATE_CHECKS = {\n    \"ms-python\": _p(\n        vscode_ext_min_version,\n        \"ms-python.python\",\n        info=(\"VSCode Integration will fail! \" \"See $[BradenM/micropy-cli#50] for details.\\n\"),\n    ),\n}\n"
  },
  {
    "path": "micropy/project/modules/__init__.py",
    "content": "\"\"\"Project Modules.\"\"\"\n\n\nfrom .modules import HookProxy, ProjectModule\nfrom .packages import DevPackagesModule, PackagesModule\nfrom .stubs import StubsModule\nfrom .templates import TemplatesModule\n\n__all__ = [\n    \"TemplatesModule\",\n    \"PackagesModule\",\n    \"StubsModule\",\n    \"ProjectModule\",\n    \"DevPackagesModule\",\n    \"HookProxy\",\n]\n"
  },
  {
    "path": "micropy/project/modules/modules.py",
    "content": "from __future__ import annotations\n\nimport abc\nimport inspect\nfrom copy import deepcopy\nfrom functools import wraps\nfrom typing import Any, Callable, List, Optional, Tuple, Type, TypeVar, Union\n\nfrom micropy import utils\nfrom micropy.config import Config\nfrom micropy.logger import Log, ServiceLog\n\n\"\"\"Project Packages Module Abstract Implementation\"\"\"\n\nT = TypeVar(\"T\")\nProxyItem = List[Tuple[T, str]]\n\n\nclass ProjectModule(metaclass=abc.ABCMeta):\n    \"\"\"Abstract Base Class for Project Modules.\"\"\"\n\n    PRIORITY: int = 0\n    _hooks: List[HookProxy] = []\n\n    def __init__(self, parent: Optional[ProjectModule] = None, log: Optional[ServiceLog] = None):\n        self._parent = parent\n        self.log = log\n\n    @property\n    def parent(self):\n        \"\"\"Component Parent.\"\"\"\n        return self._parent\n\n    @parent.setter\n    def parent(self, parent: Type[ProjectModule]) -> Type[ProjectModule]:\n        \"\"\"Sets component parent.\n\n        Args:\n            parent (Any): Parent to set\n\n        \"\"\"\n        self._parent = parent\n        return self.parent\n\n    @abc.abstractproperty\n    def config(self) -> Union[dict, Config]:\n        \"\"\"Config values specific to component.\"\"\"\n\n    @abc.abstractmethod\n    def load(self):\n        \"\"\"Method to load component.\"\"\"\n\n    @abc.abstractmethod\n    def create(self, *args: Any, **kwargs: Any) -> Any:\n        \"\"\"Method to create component.\"\"\"\n\n    @abc.abstractmethod\n    def update(self):\n        \"\"\"Method to update component.\"\"\"\n\n    # FIXME: B027\n    def add(self, component: Type[ProjectModule], *args: Any, **kwargs: Any) -> Any:  # noqa: B027\n        \"\"\"Adds component.\n\n        Args:\n            component (Any): Component to add.\n\n        \"\"\"\n\n    # FIXME: B027\n    def remove(self, component: Type[ProjectModule]) -> Any:  # noqa: B027\n        \"\"\"Removes component.\n\n        Args:\n            component (Any): Component to remove.\n\n        \"\"\"\n\n    @classmethod\n    def hook(cls, *args: Any, **kwargs: Any) -> Callable[..., Any]:\n        \"\"\"Decorator for creating a Project Hook.\n\n        Allows decorated method to be called from parent\n        container.\n\n        Returns:\n            Callable: Decorated function.\n\n        \"\"\"\n\n        def _hook(func: T) -> Callable[..., Any]:\n            name = kwargs.get(\"name\", func.__name__)\n            hook = next((i for i in cls._hooks if i._name == name), None)\n            if not hook:\n                hook = HookProxy(name)\n                ProjectModule._hooks.append(hook)\n            hook.add_method(func, **kwargs)\n\n            @wraps(func)\n            def wrapper(*args: Any, **kwargs: Any) -> T:\n                return func(*args, **kwargs)\n\n            return wrapper\n\n        return _hook\n\n    def resolve_hook(self, name: str) -> Union[Optional[HookProxy], T]:\n        \"\"\"Resolves appropriate hook for attribute name.\n\n        Args:\n            name (str): Attribute name to resolve hook for.\n\n        Returns:\n            Optional[HookProxy]: Callable Proxy for ProjectHook.\n            NoneType: Name could not be resolved.\n\n        \"\"\"\n        _hook = None\n        for hook in self._hooks:\n            if hook._name == name:\n                _hook = hook\n                _hook.add_instance(self)\n                if _hook.is_descriptor():\n                    return _hook.get()\n        return _hook\n\n\nclass HookProxy:\n    \"\"\"Proxy for Project Hooks.\n\n    Allows multiple project hooks with the same name by\n    creating individual hooks for any defined permutations\n    of kwargs.\n\n    This is accomplished by creating a unique name for each\n    permutation proxying the original attribute name to the\n    appropriate method determined from the provided kwargs.\n\n    Args:\n        name (str): Name of Proxy\n\n    \"\"\"\n\n    def __init__(self, name: str):\n        self.methods: List[ProxyItem[Callable[..., Any]]] = []\n        self.instances: List[Type[ProjectModule]] = []\n        self._name: str = name\n        self.log: ServiceLog = Log.add_logger(str(self))\n\n    def __call__(self, *args, **kwargs):\n        proxy_kwargs = deepcopy(kwargs)\n        proxy = self.resolve_proxy(**proxy_kwargs)\n        if proxy:\n            return getattr(*proxy)(*args, **kwargs)\n\n    def __str__(self):\n        name = f\"HookProxy({self._name})\"\n        return name\n\n    def __repr__(self):\n        name = f\"HookProxy(name={self._name}, methods=[{self.methods}])\"\n        return name\n\n    def resolve_proxy(self, **kwargs: Any) -> (Type[ProjectModule], str):\n        \"\"\"Resolves appropriate instance and method to proxy to.\n\n        If additional kwargs are provided and a proxy is not found,\n        the function will continue to remove one kwarg and\n        recurse into itself until either a match is found or it runs\n        out of kwargs.\n\n        Returns:\n            Instance and method name if resolved, otherwise None.\n\n        \"\"\"\n        proxy_kwargs = deepcopy(kwargs)\n        for method, name in self.methods:\n            _name = self.get_name(method, proxy_kwargs)\n            if name == _name:\n                instance = self._get_instance(method)\n                self.log.debug(f\"{self._name} proxied to [{_name}@{instance}]\")\n                return (instance, method.__name__)\n        if proxy_kwargs:\n            self.log.debug(\n                f\"could not resolve proxy: {self._name}[{proxy_kwargs}], broadening search...\"\n            )\n            proxy_kwargs.popitem()\n            return self.resolve_proxy(**proxy_kwargs)\n        return None\n\n    def _get_instance(self, attr: Callable[..., Any]) -> Optional[Type[ProjectModule]]:\n        \"\"\"Retrieves instance from attribute.\n\n        Args:\n            attr (Callable): Attribute to use.\n\n        Returns:\n            Instance the attribute belongs to.\n\n        \"\"\"\n        _class = utils.get_class_that_defined_method(attr)\n        if _class:\n            instance = next((i for i in self.instances if isinstance(i, _class)), None)\n            return instance\n\n    def is_descriptor(self) -> bool:\n        \"\"\"Determine if initial method provided is a descriptor.\"\"\"\n        method = self.methods[0][0]\n        instance = self._get_instance(method)\n        if instance:\n            attr = inspect.getattr_static(instance, self._name)\n            return inspect.isdatadescriptor(attr)\n        return False\n\n    def get(self) -> T:\n        \"\"\"Get initial method descriptor value.\"\"\"\n        instance = self._get_instance(self.methods[0][0])\n        self.log.debug(f\"{self._name} proxied to [property@{instance}]\")\n        return getattr(instance, self._name)\n\n    def add_method(self, func: Callable[..., Any], **kwargs: Any) -> ProxyItem[Callable[..., Any]]:\n        \"\"\"Adds method to Proxy.\n\n        Any kwargs provided will be used to generate the unique\n        hook name.\n\n        Args:\n            func (Callable): Method to add\n\n        Example:\n            >>> def test_func(arg1, kwarg1=False):\n                    pass\n\n            >>> self.add_method(test_func, {'kwarg1': False})\n            (test_func, '_hook__test_func__kwarg1_False')\n\n        Returns:\n            Tuple[Callable, str]: Tuple containing method and unique hook name.\n\n        \"\"\"\n        name = self.get_name(func, kwargs)\n        hook = (func, name)\n        self.methods.append(hook)\n        self.log.debug(f\"Method added to proxy: {hook}\")\n        return hook  # type: ignore\n\n    def add_instance(self, inst: Any) -> Any:\n        \"\"\"Add instance to Proxy.\n\n        Args:\n            inst (Any): Instance to add.\n\n        \"\"\"\n        return self.instances.append(inst)\n\n    def get_name(self, func: Callable[..., Any], params: Optional[dict] = None) -> str:\n        \"\"\"Generates name from method and provided kwargs.\n\n        Args:\n            func (Callable): Method to generate name for.\n            params (Dict[Any, Any], optional): Any kwargs to update the defaults with.\n                Defaults to None. If none, uses default kwargs.\n\n        Returns:\n            str: Generated name\n\n        \"\"\"\n        params = params or {}\n        sig = inspect.signature(func)\n        _default = {\n            p.name: p.default\n            for p in sig.parameters.values()\n            if p.kind == p.POSITIONAL_OR_KEYWORD and p.default is not p.empty\n        }\n        params = {**_default, **params}\n        name = f\"_hook__{self._name}__{'__'.join(f'{k}_{v}' for k, v in params.items())}\"\n        return name\n"
  },
  {
    "path": "micropy/project/modules/packages.py",
    "content": "\"\"\"Project Packages Module.\"\"\"\n\nimport shutil\nfrom pathlib import Path\nfrom typing import Any, Optional, Union\n\nfrom boltons import fileutils\nfrom micropy import utils\nfrom micropy.config import Config\nfrom micropy.packages import (\n    LocalDependencySource,\n    Package,\n    PackageDependencySource,\n    create_dependency_source,\n)\nfrom micropy.project.modules import ProjectModule\n\n\nclass PackagesModule(ProjectModule):\n    \"\"\"Project Module for handling requirements.\n\n    Args:\n        path (str): Path to create requirements file at.\n        packages (dict, optional): Initial packages to use.\n            Defaults to None.\n\n    \"\"\"\n\n    name: str = \"packages\"\n    PRIORITY: int = 7\n\n    def __init__(self, path, **kwargs):\n        super().__init__(**kwargs)\n        self._path = Path(path)\n\n    @property\n    def packages(self):\n        _packages = self.config.get(self.name, {})\n        return _packages\n\n    @property\n    def path(self):\n        \"\"\"Path to requirements file.\n\n        Returns:\n            Path: Path to file\n\n        \"\"\"\n        path = self.parent.path / self._path\n        return path\n\n    @property\n    def pkg_path(self):\n        \"\"\"Path to package data folder.\n\n        Returns:\n            Path: Path to folder.\n\n        \"\"\"\n        return self.parent.data_path / self.parent.name\n\n    @property\n    def config(self) -> Config:\n        \"\"\"Config values specific to component.\n\n        Returns:\n            Component config.\n\n        \"\"\"\n        return self.parent.config\n\n    @property\n    def context(self) -> Config:\n        \"\"\"Context values specific to component.\n\n        Returns:\n            Context values.\n\n        \"\"\"\n        return self.parent.context\n\n    @property\n    def cache(self) -> Config:\n        \"\"\"Project Cache.\n\n        Returns:\n            Project wide cache\n\n        \"\"\"\n        return self.parent.cache\n\n    def install_package(self, source: Union[LocalDependencySource, PackageDependencySource]) -> Any:\n        with source as files:\n            if source.is_local:\n                self.log.debug(f\"installing {source} as local\")\n                return\n            if isinstance(files, list):\n                self.log.debug(f\"installing {source} as module(s)\")\n                # Iterates over flattened list of stubs tuple\n                file_paths = [(f, (self.pkg_path / f.name)) for f in list(sum(files, ()))]\n                for paths in file_paths:\n                    shutil.move(*paths)  # overwrites if existing\n                return file_paths\n            self.log.debug(f\"installing {source} as package\")\n            pkg_path = self.pkg_path / source.package.name\n            return fileutils.copytree(files, pkg_path)\n\n    @ProjectModule.hook(dev=False)\n    def add_from_file(self, path: Optional[Path] = None, dev: bool = False, **kwargs: Any) -> dict:\n        \"\"\"Loads all requirements from file.\n\n        Args:\n            path: Path to file. Defaults to self.path.\n            dev: If dev requirements should be loaded.\n                Defaults to False.\n\n        \"\"\"\n        path = path or self.path\n        reqs = utils.iter_requirements(path)\n        self.log.debug(f\"loading requirements from: {path}\")\n        for r in reqs:\n            pkg = create_dependency_source(r.line).package\n            if not self.packages.get(pkg.name):\n                self.config.add(self.name + \"/\" + pkg.name, pkg.pretty_specs)\n                if pkg.editable:\n                    self.context.extend(\"local_paths\", [pkg.path], unique=True)\n        return self.packages\n\n    @ProjectModule.hook()\n    def add_package(self, package, dev=False, **kwargs):\n        \"\"\"Add requirement to project.\n\n        Args:\n            package (str): package name/spec\n            dev (bool, optional): If dev requirements should be loaded.\n                Defaults to False.\n\n        Returns:\n            dict: Dictionary of packages\n\n        \"\"\"\n        self.log.debug(f\"adding new dependency: {package}\")\n        source = create_dependency_source(package, **kwargs)\n        pkg = source.package\n        self.log.info(f\"Adding $[{pkg.name}] to requirements...\")\n        if self.packages.get(pkg.name, None):\n            self.log.error(f\"$[{pkg}] is already installed!\")\n            self.update()\n            return None\n        self.config.add(self.name + \"/\" + pkg.name, pkg.pretty_specs)\n        try:\n            self.load()\n        except Exception as e:\n            self.log.error(f\"Failed to install: {pkg.name}\", exception=e)\n            self.config.pop(self.name + \"/\" + pkg.name)\n            raise\n        else:\n            if pkg.editable:\n                self.context.extend(\"local_paths\", [pkg.path], unique=True)\n            self.log.success(\"Package installed!\")\n        finally:\n            self.parent.update()\n            # TODO: B012\n            return self.packages  # noqa\n\n    def load(self, fetch=True, **kwargs):\n        \"\"\"Retrieves and stubs project requirements.\"\"\"\n        self.pkg_path.mkdir(parents=True, exist_ok=True)\n        if self.path.exists():\n            self.add_from_file(self.path)\n        pkg_keys = set(self.packages.keys())\n        pkg_cache = self.cache.get(self.name)\n        new_pkgs = pkg_keys.copy()\n        if pkg_cache:\n            new_pkgs = new_pkgs - set(pkg_cache)\n        new_packages = [\n            Package.from_text(name, spec)\n            for name, spec in self.packages.items()\n            if name in new_pkgs\n        ]\n        if fetch:\n            if new_packages:\n                self.log.title(\"Fetching Requirements\")\n            for req in new_packages:\n\n                def format_desc(p):\n                    return \"\".join(self.log.iter_formatted(f\"$B[{p}]\"))\n\n                source = create_dependency_source(\n                    str(req),\n                    name=req.name,\n                    format_desc=lambda p: f\"{self.log.get_service()} {format_desc(p)}\",\n                )\n                self.install_package(source)\n        self.update()\n        self.cache.upsert(self.name, list(pkg_keys))\n\n    def create(self):\n        \"\"\"Create project files.\"\"\"\n        self.pkg_path.mkdir(parents=True, exist_ok=True)\n        if not self.config.get(self.name):\n            self.config.add(self.name, {})\n        return self.update()\n\n    def update(self):\n        \"\"\"Dumps packages to file at path.\"\"\"\n        if not self.path.exists():\n            self.path.touch()\n        pkgs = [Package.from_text(name, spec) for name, spec in self.config.get(self.name).items()]\n        self.log.debug(f\"dumping to {self.path.name}\")\n        with self.path.open(\"r+\") as f:\n            content = [c.strip() for c in f.readlines() if c.strip() != \"\"]\n            _lines = sorted({str(p) for p in pkgs} | set(content))\n            lines = [line + \"\\n\" for line in _lines]\n            self.log.debug(f\"dumping: {lines}\")\n            f.seek(0)\n            f.writelines(lines)\n        local_paths = [p.path for p in pkgs if p.editable]\n        if local_paths:\n            self.context.add(\"local_paths\", local_paths)\n        self.context.extend(\"paths\", [self.pkg_path], unique=True)\n\n\nclass DevPackagesModule(PackagesModule):\n    \"\"\"Project Module for Dev Packages.\"\"\"\n\n    PRIORITY: int = 8\n\n    def __init__(self, path, **kwargs):\n        super().__init__(path, **kwargs)\n        self.name = \"dev-packages\"\n\n    def create(self):\n        \"\"\"Creates component.\"\"\"\n        self.config.add(f\"{self.name}/micropy-cli\", \"*\")\n        super().create()\n\n    def load(self, *args, **kwargs):\n        \"\"\"Load component.\"\"\"\n        super().load(*args, **kwargs, fetch=False)\n\n    @ProjectModule.hook(dev=True)\n    def add_package(self, package, **kwargs):\n        \"\"\"Adds package.\"\"\"\n        return super().add_package(package, **kwargs)\n\n    @ProjectModule.hook(dev=True)\n    def add_from_file(self, path=None, **kwargs):\n        \"\"\"Adds packages from file.\"\"\"\n        return super().add_from_file(path=path, **kwargs)\n"
  },
  {
    "path": "micropy/project/modules/stubs.py",
    "content": "\"\"\"Project Stubs Module.\"\"\"\n\nimport sys\nfrom pathlib import Path\nfrom typing import Any, List, Sequence, Union\n\nfrom boltons import setutils\nfrom micropy.project.modules import ProjectModule\nfrom micropy.stubs import StubManager\nfrom micropy.stubs.stubs import DeviceStub\n\n\nclass StubsModule(ProjectModule):\n    \"\"\"Project module for handling Stubs.\n\n    Args:\n        stub_manager (StubManager): StubManager instance.\n        stubs (List[Type[Stub]], optional): Initial Stubs to use.\n\n    \"\"\"\n\n    PRIORITY: int = 9\n\n    def __init__(\n        self, stub_manager: StubManager, stubs: Sequence[DeviceStub] = None, **kwargs: Any\n    ):\n        super().__init__(**kwargs)\n        self.stub_manager: StubManager = stub_manager\n        self._stubs: Sequence[DeviceStub] = stubs or []\n\n    @property\n    def context(self):\n        \"\"\"Component stub context.\"\"\"\n        return self.parent.context\n\n    @property\n    def config(self) -> dict:\n        \"\"\"Component specific config values.\n\n        Returns:\n            dict: Current config.\n\n        \"\"\"\n        return self.parent.config\n\n    @property\n    @ProjectModule.hook()\n    def stubs(self) -> Union[StubManager, Sequence[DeviceStub]]:\n        \"\"\"Component stubs.\n\n        Returns:\n            List[micropy.stubs.Stub]: List of stubs used in project.\n\n        \"\"\"\n        _stubs = self.context.get(\"stubs\", [])\n        return self._resolve_subresource(_stubs)\n\n    def get_stub_tree(self, stubs) -> Sequence[Path]:\n        \"\"\"Retrieve and order paths to base stubs and any stubs they depend on.\n\n        Args:\n            stubs: List of Stub Items\n\n        Returns:\n            Paths to all stubs project depends on.\n\n        \"\"\"\n        stub_tree = setutils.IndexedSet()\n        base_stubs = setutils.IndexedSet([s.stubs for s in stubs])\n        frozen = [s.frozen for s in stubs]\n        fware_mods = [s.firmware.frozen for s in stubs if s.firmware is not None]\n        stub_tree.update(*frozen, *fware_mods, *base_stubs)\n        return list(stub_tree)\n\n    def _resolve_subresource(\n        self, stubs: List[DeviceStub]\n    ) -> Union[StubManager, Sequence[DeviceStub]]:\n        \"\"\"Resolves stub resource.\n\n        Args:\n            stubs (stubs): Stubs Passed to Manager\n\n        \"\"\"\n        if not hasattr(self, \"_parent\"):\n            return self._stubs\n        if not self.parent.exists:\n            return self._stubs\n        try:\n            resource = set(self.stub_manager.resolve_subresource(stubs, self.parent.data_path))\n        except OSError as e:\n            self.log.error(\"Failed to Create Stub Links!\", exception=e)\n            sys.exit(1)\n        else:\n            self.config.upsert(\"stubs\", {s.name: s.stub_version for s in stubs})\n            return resource\n\n    def _load_stub_data(self, stub_data=None, **kwargs):\n        \"\"\"Loads Serialized Stub Data.\n\n        Args:\n            stub_data (dict): Dict of Stubs\n\n        \"\"\"\n        for name, location in stub_data.items():\n            _path = Path(location).absolute()\n            if Path(_path).exists():\n                yield self.stub_manager.add(_path)\n            yield self.stub_manager.add(name)\n\n    def load(self, **kwargs):\n        \"\"\"Loads stubs from info file.\n\n        Args:\n            stub_list (dict): Dict of Stubs\n\n        \"\"\"\n        self.config.upsert(\"stubs\", {s.name: s.stub_version for s in self._stubs})\n        stubs = list(self._load_stub_data(stub_data=self.config.get(\"stubs\")))\n        stubs.extend(self.stubs)\n        stubs = self._resolve_subresource(stubs)\n        self.context.upsert(\"stubs\", stubs)\n        self.context.upsert(\"paths\", self.get_stub_tree(self.stubs))\n        return self.stubs\n\n    def create(self):\n        \"\"\"Create stub project files.\"\"\"\n        self.log.info(f\"Stubs: $[{' '.join(str(s) for s in self.stubs)}]\")\n        return self.load()\n\n    def update(self):\n        \"\"\"Update current project stubs.\"\"\"\n        self.load()\n        return self.stubs\n\n    @ProjectModule.hook()\n    def add_stub(self, stub, **kwargs):\n        \"\"\"Add stub to project.\n\n        Args:\n            stub (Stub): Stub object to add\n\n        Returns:\n            [Stubs]: Project Stubs\n\n        \"\"\"\n        self.context.extend(\"stubs\", [stub])\n        self.log.info(\"Loading project...\")\n        self._resolve_subresource(self.stubs)\n        self.log.info(\"Updating Project Info...\")\n        self.parent.update()\n        self.log.info(f\"Project Stubs: $[{' '.join(str(s) for s in self.stubs)}]\")\n        self.log.success(\"\\nProject Updated!\")\n        return self.stubs\n"
  },
  {
    "path": "micropy/project/modules/templates.py",
    "content": "\"\"\"Project Templates Module.\"\"\"\n\n\nfrom micropy.project.modules import ProjectModule\nfrom micropy.project.template import TemplateProvider\n\n\nclass TemplatesModule(ProjectModule):\n    \"\"\"Project Templates Module.\n\n    Generates and manages project files using the Projects\n    context.\n\n    Args:\n        templates (List[str]): List of templates to use.\n        run_checks (bool, optional): Whether to execute checks or not.\n            Defaults to True.\n\n    \"\"\"\n\n    PRIORITY: int = 1\n    TEMPLATES = TemplateProvider.TEMPLATES\n    _dynamic = [\"vscode\", \"pylint\"]\n\n    def __init__(self, templates=None, run_checks=True, **kwargs):\n        self._templates = templates or []\n        super().__init__(**kwargs)\n        self.run_checks = run_checks\n\n    @property\n    def config(self):\n        \"\"\"Template config.\n\n        Returns:\n            dict: Current configuration\n\n        \"\"\"\n        return self.parent.config\n\n    def get_provider(self, templates):\n        return TemplateProvider(templates, run_checks=self.run_checks, log=self.log)\n\n    def load(self, **kwargs):\n        \"\"\"Loads project templates.\"\"\"\n        self.provider = self.get_provider(self.config.get(\"config\"))\n        templates = [k for k, v in self.config.get(\"config\").items() if v]\n        self.log.debug(f\"Loading Templates: {templates}\")\n        self.provider = TemplateProvider(templates, **kwargs)\n        self.update()\n\n    def create(self):\n        \"\"\"Generates project files.\n\n        Returns:\n            dict: Project context\n\n        \"\"\"\n        self.log.title(\"Rendering Templates\")\n        self.log.info(\"Populating Stub Info...\")\n        for key in self._templates:\n            if key in self._dynamic:\n                self.config.add(\"config\" + \"/\" + key, True)\n        self.provider = self.get_provider(self._templates)\n        for t in self.provider.templates:\n            self.provider.render_to(t, self.parent.path, **self.parent.context.raw())\n        self.log.success(\"Stubs Injected!\")\n        return self._templates\n\n    def update(self):\n        \"\"\"Updates project files.\n\n        Returns:\n            dict: Project context\n\n        \"\"\"\n        self.provider = self.get_provider(self.config.get(\"config\"))\n        self.log.debug(f\"updating templates with context: {self.parent.context.raw()}\")\n        for tmp in self.provider.templates:\n            self.provider.update(tmp, self.parent.path, **self.parent.context.raw())\n        return self.parent.context\n"
  },
  {
    "path": "micropy/project/project.py",
    "content": "\"\"\"Hosts functionality relating to generation of user projects.\"\"\"\n\nfrom pathlib import Path\nfrom typing import Any, Iterator, List, Optional, Type\n\nfrom boltons.queueutils import PriorityQueue\nfrom micropy.config import Config, DictConfigSource\nfrom micropy.logger import Log, ServiceLog\nfrom micropy.project.modules import ProjectModule\n\n\nclass Project(ProjectModule):\n    \"\"\"Micropy Project.\n\n    Args:\n        path (str): Path to project root.\n        name (str, optional): Name of Project.\n            Defaults to None. If none, uses name of current directory.\n\n    \"\"\"\n\n    def __init__(self, path: str, name: Optional[str] = None, **kwargs: Any):\n        self._children: List[Type[ProjectModule]] = []\n        self.path: Path = Path(path).absolute()\n        self.data_path: Path = self.path / \".micropy\"\n        self.info_path: Path = self.path / \"micropy.json\"\n        self.cache_path: Path = self.data_path / \".cache\"\n        self._cache = Config(self.cache_path)\n        self._context = Config(source_format=DictConfigSource, default={\"datadir\": self.data_path})\n        self.name: str = name or self.path.name\n        default_config = {\n            \"name\": self.name,\n        }\n        self._config: Config = Config(self.info_path, default=default_config)\n        self.log: ServiceLog = Log.add_logger(self.name, show_title=False)\n\n    def __getattr__(self, name: str) -> Any:\n        results = iter([c.resolve_hook(name) for c in self._children])\n        for res in results:\n            if res is not None:\n                self.log.debug(f\"Hook Resolved: {name} -> {res}\")\n                return res\n        return self.__getattribute__(name)\n\n    @property\n    def exists(self) -> bool:\n        \"\"\"Whether this project exists.\n\n        Returns:\n            bool: True if it exists\n\n        \"\"\"\n        return self.info_path.exists()\n\n    @property\n    def config(self) -> Config:\n        \"\"\"Project Configuration.\n\n        Returns:\n            Config: Project Config Instance\n\n        \"\"\"\n        return self._config\n\n    @property\n    def context(self) -> Config:\n        \"\"\"Project context used in templates.\n\n        Returns:\n            Config: Current context\n\n        \"\"\"\n        return self._context\n\n    @property\n    def cache(self) -> Config:\n        \"\"\"Project wide cache.\n\n        Returns:\n            Cache instance\n\n        \"\"\"\n        return self._cache\n\n    def iter_children_by_priority(self) -> Iterator[Type[ProjectModule]]:\n        \"\"\"Iterate project modules by priority.\n\n        Yields:\n            the next child item\n\n        \"\"\"\n        pq = PriorityQueue()\n        for i in self._children:\n            pq.add(i, i.PRIORITY)\n        more = pq.peek(default=False)\n        while more:\n            yield pq.pop()\n            more = pq.peek(default=False)\n\n    def add(self, component, *args, **kwargs):\n        \"\"\"Adds project component.\n\n        Args:\n            component (Any): Component to add.\n\n        \"\"\"\n        child = component(*args, **kwargs, log=self.log, parent=self)\n        self._children.append(child)\n        self.log.debug(f\"adding module: {type(child).__name__}\")\n\n    def remove(self, component):\n        \"\"\"Removes project component.\n\n        Args:\n            component (Any): Component to remove.\n\n        \"\"\"\n        child = next(i for i in self._children if isinstance(i, component))\n        self._children.remove(child)\n\n    def load(self, **kwargs: Any) -> \"Project\":\n        \"\"\"Loads all components in Project.\n\n        Returns:\n            Current Project Instance\n\n        \"\"\"\n        self.name = self._config.get(\"name\")\n        self.data_path.mkdir(exist_ok=True)\n        for child in self.iter_children_by_priority():\n            child.load(**kwargs)\n        return self\n\n    def create(self):\n        \"\"\"Creates new Project.\n\n        Returns:\n            Path: Path relative to current active directory.\n\n        \"\"\"\n        self.log.title(f\"Initiating $[{self.name}]\")\n        self.data_path.mkdir(exist_ok=True, parents=True)\n        ignore_data = self.data_path / \".gitignore\"\n        ignore_data.write_text(\"*\")\n        self.log.debug(f\"Generated Project Context: {self.context}\")\n        for child in self.iter_children_by_priority():\n            child.create()\n        self.info_path.touch()\n        self.config.sync()\n        self.log.success(\"Project Created!\")\n        return self.path\n\n    def update(self):\n        \"\"\"Updates all project components.\n\n        Returns:\n            Current active project.\n\n        \"\"\"\n        self.log.debug(\"Updating all project modules...\")\n        for child in self.iter_children_by_priority():\n            child.update()\n        return self\n"
  },
  {
    "path": "micropy/project/template/.gitignore",
    "content": "\n# Created by https://www.gitignore.io/api/python,visualstudiocode\n# Edit at https://www.gitignore.io/?templates=python,visualstudiocode\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n### VisualStudioCode ###\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n### VisualStudioCode Patch ###\n# Ignore all local history of files\n.history\n\n# End of https://www.gitignore.io/api/python,visualstudiocode\n\n### Micropy Cli ###\n.micropy/\n!micropy.json\n!src/lib\n"
  },
  {
    "path": "micropy/project/template/.pylintrc",
    "content": "[MAIN]\n# Loaded Stubs: {% for stub in stubs %} {{ stub }} {% endfor %}\ninit-hook='import sys;sys.path[1:1]=[\"src/lib\",{% for path in paths %}\"{{ path|replace(\"\\\\\", \"/\") }}\", {% endfor %} ]'\n\n[MESSAGES CONTROL]\n# Only show warnings with the listed confidence levels. Leave empty to show\n# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.\nconfidence=INFERENCE\n\n# Disable the message, report, category or checker with the given id(s). You\n# can either give multiple identifiers separated by comma (,) or put this\n# option multiple times (only on the command line, not in the configuration\n# file where it should appear only once). You can also use \"--disable=all\" to\n# disable everything first and then reenable specific checks. For example, if\n# you want to run only the similarities checker, you can use \"--disable=all\n# --enable=similarities\". If you want to run only the classes checker, but have\n# no Warning level messages displayed, use \"--disable=all --enable=classes\n# --disable=W\".\n\ndisable = missing-docstring, line-too-long, trailing-newlines, broad-except, logging-format-interpolation, invalid-name, empty-docstring,\n        no-method-argument, assignment-from-no-return, too-many-function-args, unexpected-keyword-arg\n        # the 2nd  line deals with the limited information in the generated stubs.\n"
  },
  {
    "path": "micropy/project/template/.vscode/extensions.json",
    "content": "{\n    // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.\n    // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp\n    // List of extensions which should be recommended for users of this workspace.\n    \"recommendations\": [\n        \"ms-python.python\", // micropy-cli: required for vscode micropython integrations\n        \"VisualStudioExptTeam.vscodeintellicode\" // micropy-cli: optional for advanced intellisense\n    ]\n}\n"
  },
  {
    "path": "micropy/project/template/.vscode/settings.json",
    "content": "{\n    // Loaded Stubs: {% for stub in stubs %} {{ stub }} {% endfor %}\n    {% if language_server == 'pylance' %}\n    \"python.languageServer\": \"Pylance\",\n    \"python.analysis.autoSearchPath\": true,\n    \"python.autoComplete.extraPaths\": {{ paths }},\n    \"python.analysis.diagnosticSeverityOverrides\": { \"reportMissingModuleSource\": \"none\" },\n    \"python.analysis.typeCheckingMode\": \"basic\",\n    \"python.autoComplete.typeshedPaths\":  {{ typeshed_paths }},\n    \"python.analysis.typeshedPaths\":  {{ typeshed_paths }},\n    {% endif %}\n    {% if language_server == 'mpls' %}\n    \"python.jediEnabled\": false,\n    \"python.autoComplete.typeshedPaths\":  {{ paths }},\n    \"python.analysis.typeshedPaths\":  {{ paths }},\n    {% endif %}\n\n    \"python.linting.enabled\": true,\n    \"python.linting.pylintEnabled\": true\n}\n"
  },
  {
    "path": "micropy/project/template/pymakr.conf",
    "content": "{\n    \"address\": \"192.168.4.1\",\n    \"username\": \"micro\",\n    \"password\": \"python\",\n    \"sync_folder\": \"src\",\n    \"open_on_start\": true,\n    \"safe_boot_on_upload\": false,\n    \"py_ignore\": [\n        \"pymakr.conf\",\n        \".vscode\",\n        \".gitignore\",\n        \".git\",\n        \"project.pymakr\",\n        \"env\",\n        \"venv\",\n        \".python-version\",\n        \".micropy/\",\n        \"micropy.json\"\n    ],\n    \"fast_upload\": false\n}\n"
  },
  {
    "path": "micropy/project/template/src/boot.py",
    "content": "# boot.py - - runs on boot-up\n"
  },
  {
    "path": "micropy/project/template/src/main.py",
    "content": "# main.py\n"
  },
  {
    "path": "micropy/project/template.py",
    "content": "\"\"\"Module for handling jinja2 and MicroPy Templates.\"\"\"\n\nimport json\nimport os\nfrom itertools import chain\nfrom pathlib import Path\nfrom typing import Iterator, List, Literal, Union\n\nfrom jinja2 import Environment, FileSystemLoader\nfrom micropy.logger import Log\n\n\nclass Template:\n    \"\"\"Base Template Builder Class.\n\n    Args:\n        template (jinja2.Template): Jinja2 Template Instance\n\n    Raises:\n        NotImplementedError: Method must be overridden by subclass\n\n    \"\"\"\n\n    FILENAME = None\n    CHECKS = []\n\n    def __init__(self, template, **kwargs):\n        self.template = template\n        self.stubs = kwargs.get(\"stubs\", [])\n        self.paths = kwargs.get(\"paths\", [])\n        self.datadir = kwargs.get(\"datadir\", None)\n        self.local_paths = kwargs.get(\"local_paths\", [])\n\n    @property\n    def context(self):\n        \"\"\"Context for template.\"\"\"\n        raise NotImplementedError\n\n    def iter_clean(self, data=None):\n        \"\"\"Yields cleaned data.\n\n        Args:\n            data (str, optional): Alternative data to clean.\n                Defaults to None. If none, uses template render.\n\n        \"\"\"\n        render = data or self.template.render(self.context)\n        for line in render.splitlines(True):\n            _line = line.strip()\n            if not _line.startswith(\"//\"):\n                yield line\n\n    def run_checks(self):\n        \"\"\"Runs all template checks.\n\n        Returns:\n            bool: True if all checks passed\n\n        \"\"\"\n        if not self.CHECKS:\n            return True\n        results = [not ck() for ck in self.CHECKS]\n        return any(results)\n\n    def update(self, root):\n        \"\"\"Update Template File.\n\n        Args:\n            root (str): Path to project root\n\n        Raises:\n            NotImplementedError: Raised if Subclass has not Implemented Update\n\n        Returns:\n            func: Template Update Func\n\n        \"\"\"\n        update_func = getattr(self, \"update_method\", None)\n        update_kwargs = getattr(self, \"update_kwargs\", {})\n        if not update_func:\n            return None\n        path = root / self.FILENAME\n        return update_func(path, **update_kwargs)\n\n    def update_as_json(self, path):\n        \"\"\"Update template file as JSON.\n\n        Args:\n            path (str): File path to update\n\n        \"\"\"\n        render = json.loads(\"\".join(self.iter_clean()))\n        data = json.loads(\"\".join(self.iter_clean(path.read_text())))\n        data.update(render)\n        with path.open(\"w+\") as f:\n            json.dump(data, f, indent=4)\n\n    def update_as_text(self, path, by_contains=None):\n        \"\"\"Update template file as text.\n\n        Args:\n            path (str): file path to update.\n            by_contains ([str], optional): Update lines that contain a string.\n             Defaults to None.\n\n        \"\"\"\n        r_lines = list(self.iter_clean())\n        upd_lines = []\n        if by_contains:\n            upd_lines = [\n                r_lines.index(line) for line in r_lines if any(i in line for i in by_contains)\n            ]\n        with path.open(\"r+\") as f:\n            c_lines = self.iter_clean(f.read())\n            f.seek(0)\n            for it, line in enumerate(c_lines):\n                _line = line\n                if it in upd_lines:\n                    _line = r_lines[it]\n                f.write(_line)\n\n    def render_stream(self):\n        \"\"\"Returns template stream from context.\"\"\"\n        stream = self.template.stream(self.context)\n        return stream\n\n    def iter_relative_paths(self, paths: List[Path], strict: bool = False) -> Iterator[Path]:\n        \"\"\"Iterate over a list of paths relative to project root.\n\n        Args:\n            paths: List of paths to make relative.\n            strict: Raises ValueError if True and path cannot be made relative.\n                Defaults to False.\n\n        Raises:\n            ValueError: Path could not be made relative and `strict` is True.\n\n        Yields:\n            Path relative to project root.\n\n        \"\"\"\n        for p in paths:\n            path = p\n            if not p.is_absolute():\n                path = Path(os.path.relpath(path)).resolve()\n            try:\n                yield path.relative_to(self.datadir.parent)\n            except ValueError:\n                yield path\n\n    def __str__(self):\n        cls_name = self.__class__.__name__\n        return f\"{cls_name}[{self.template.name}]::[{self.context}]\"\n\n\nclass GenericTemplate(Template):\n    \"\"\"Generic Template for files without context.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.FILENAME = self.template.name\n\n    @property\n    def context(self):\n        \"\"\"Empty Context.\"\"\"\n        return {}\n\n\nclass CodeTemplate(Template):\n    \"\"\"Template for VSCode settings.\"\"\"\n\n    FILENAME = \".vscode/settings.json\"\n\n    # TODO: rewrite this module and have proper DI\n    language_server: Union[Literal[\"mpls\"], Literal[\"pylance\"]]\n\n    def __init__(self, *args, **kwargs):\n        self.update_method = self.update_as_json\n        # TODO: but for now, assume pylance.\n        self.language_server = kwargs.get(\"language_server\", \"pylance\")\n        super().__init__(*args, **kwargs)\n\n    @property\n    def context(self):\n        \"\"\"VScode Config Context.\"\"\"\n        paths = self.paths\n        if self.datadir:\n            paths = list(self.iter_relative_paths(self.paths, strict=True))\n        if self.local_paths:\n            paths.extend(self.iter_relative_paths(self.local_paths))\n        stub_paths = json.dumps([str(s) for s in paths])\n        ctx = {\n            \"stubs\": self.stubs or [],\n            \"paths\": stub_paths,\n            \"typeshed_paths\": json.dumps([str(s) for s in [*paths, \"typings\"]]),\n            \"language_server\": self.language_server,\n        }\n        return ctx\n\n\nclass PylintTemplate(Template):\n    \"\"\"Template for Pylint settings.\"\"\"\n\n    FILENAME = \".pylintrc\"\n\n    def __init__(self, *args, **kwargs):\n        self.update_method = self.update_as_text\n        self.update_kwargs = {\"by_contains\": [\"sys.path[1:1]\"]}\n        super().__init__(*args, **kwargs)\n\n    @property\n    def context(self):\n        \"\"\"Pylint Config Context.\"\"\"\n        paths = self.paths\n        if self.datadir:\n            paths = list(self.iter_relative_paths(self.paths, strict=True))\n        if self.local_paths:\n            paths.extend(self.iter_relative_paths(self.local_paths))\n        ctx = {\"stubs\": self.stubs or [], \"paths\": paths or []}\n        return ctx\n\n\nclass TemplateProvider:\n    \"\"\"Template Provider.\"\"\"\n\n    _template_files = {\n        \"vscode\": CodeTemplate,\n        \"pylint\": PylintTemplate,\n        \"vsextensions\": \".vscode/extensions.json\",\n        \"pymakr\": \"pymakr.conf\",\n        \"main\": \"src/main.py\",\n        \"boot\": \"src/boot.py\",\n        \"gitignore\": \".gitignore\",\n    }\n    ENVIRONMENT = None\n    TEMPLATE_DIR = Path(__file__).parent / \"template\"\n    TEMPLATES = {\n        \"vscode\": (\n            [\"vscode\", \"vsextensions\"],\n            \"VSCode Settings for Autocompletion/Intellisense\",\n        ),\n        \"pymakr\": ([\"pymakr\"], \"Pymakr Configuration\"),\n        \"pylint\": ([\"pylint\"], \"Pylint MicroPython Settings\"),\n        \"gitignore\": ([\"gitignore\"], \"Git Ignore Template\"),\n        \"bootstrap\": ([\"main\", \"boot\"], \"main.py & boot.py files\"),\n    }\n\n    def __init__(self, templates, log=None, **kwargs):\n        \"\"\"Template Factory.\n\n        Args:\n            templates ([str]): List of Templates to use\n            log (callable, optional): Log instance to use.\n                Defaults to None. If none, creates a new one.\n            run_checks (bool, optional): Whether to run template checks.\n                Defaults to True.\n\n        \"\"\"\n        self.run_checks = kwargs.get(\"run_checks\", True)\n        self.template_names = set(\n            chain.from_iterable([self.TEMPLATES.get(t)[0] for t in templates])\n        )\n        self.files = {k: v for k, v in self._template_files.items() if k in self.template_names}\n        self.log = log or Log.add_logger(\"Templater\")\n        if self.__class__.ENVIRONMENT is None:\n            loader = FileSystemLoader(str(self.TEMPLATE_DIR))\n            self.__class__.ENVIRONMENT = Environment(loader=loader)\n            self.log.debug(\"Created Jinja2 Environment\")\n            self.log.debug(f\"Detected Templates: {self.ENVIRONMENT.list_templates()}\")\n\n    def get(self, name, *args, **kwargs):\n        \"\"\"Retrieve appropriate Template instance by name.\n\n        Args:\n            name (str): Name of template\n\n        Returns:\n            Template: Template instance\n\n        \"\"\"\n        temp_def = self.files.get(name)\n        file_attr = getattr(temp_def, \"FILENAME\", None)\n        filename = temp_def if file_attr is None else file_attr\n        temp_cls = GenericTemplate if file_attr is None else temp_def\n        file_temp = self.ENVIRONMENT.get_template(filename)\n        self.log.debug(f\"Retrieving {name} as {temp_cls} from {file_temp.name}\")\n        template = temp_cls(file_temp, *args, **kwargs)\n        return template\n\n    def render_to(self, name, parent_dir, *args, **kwargs):\n        \"\"\"Renders Template to a file under parent directory.\n\n        Args:\n            name (str): Name of template\n            parent_dir (str): Path to root dir\n\n        \"\"\"\n        template = self.get(name, **kwargs)\n        self.log.debug(f\"Loaded: {str(template)}\")\n        if self.run_checks:\n            self.log.debug(f\"Verifying {template} requirements...\")\n            template.run_checks()\n        out_dir = parent_dir / template.FILENAME\n        if not os.path.isfile(out_dir):\n            self.log.debug(f\"Create: {out_dir}\")\n            parent_dir.mkdir(exist_ok=True)\n            out_dir.parent.mkdir(exist_ok=True, parents=True)\n            self.log.debug(f\"Rendered: {name} to {str(out_dir)}\")\n            self.log.info(f\"$[{name.capitalize()}] File Generated!\")\n            stream = template.render_stream()\n            return stream.dump(str(out_dir))\n        else:\n            self.log.debug(f\"Update: {out_dir}\")\n            template.update(parent_dir)\n            self.log.info(f\"$[{name.capitalize()}] File Updated!\")\n\n    def update(self, name, root_dir, **kwargs):\n        \"\"\"Update existing Template.\n\n        Args:\n            name (str): Template name\n            root_dir (str): Path to project root\n\n        Returns:\n            Template: Updated Template Instance\n\n        \"\"\"\n        template = self.get(name, **kwargs)\n        self.log.debug(f\"Loaded: {str(template)}\")\n        try:\n            template.update(root_dir)\n        except FileNotFoundError:\n            self.log.debug(\"Template does not exist!\")\n            return self.render_to(name, root_dir, **kwargs)\n        self.log.debug(f\"Updated: {str(template)}\")\n        return template\n\n    @property\n    def templates(self):\n        \"\"\"returns all template names.\"\"\"\n        return self.files.keys()\n"
  },
  {
    "path": "micropy/py.typed",
    "content": ""
  },
  {
    "path": "micropy/pyd/__init__.py",
    "content": "\"\"\"Module for interfacing with py-devices.\"\"\"\n\nfrom .abc import (\n    DevicePath,\n    HostPath,\n    MessageConsumer,\n    MetaPyDevice,\n    MetaPyDeviceBackend,\n    PyDeviceConsumer,\n    StreamConsumer,\n)\nfrom .consumers import ConsumerDelegate, MessageHandlers, ProgressStreamConsumer, StreamHandlers\nfrom .pydevice import PyDevice\n\n__all__ = [\n    \"PyDevice\",\n    \"ConsumerDelegate\",\n    \"ProgressStreamConsumer\",\n    \"StreamHandlers\",\n    \"MessageHandlers\",\n    \"PyDeviceConsumer\",\n    \"MessageConsumer\",\n    \"StreamConsumer\",\n    \"MetaPyDevice\",\n    \"MetaPyDeviceBackend\",\n    \"DevicePath\",\n    \"HostPath\",\n]\n"
  },
  {
    "path": "micropy/pyd/abc.py",
    "content": "from __future__ import annotations\n\nimport abc\nfrom io import BytesIO, StringIO\nfrom pathlib import Path\nfrom typing import Any, AnyStr, Generic, NewType, Protocol, TypeVar\n\nHostPath = NewType(\"HostPath\", str)\nDevicePath = NewType(\"DevicePath\", str)\n\n\nclass StartHandler(Protocol):\n    def __call__(self, *, name: str = None, size: int | None = None) -> Any:\n        ...\n\n\nclass UpdateHandler(Protocol):\n    def __call__(self, *, size: int | None = None) -> Any:\n        ...\n\n\nclass EndHandler(Protocol):\n    def __call__(self) -> Any:\n        ...\n\n\nclass MessageHandler(Protocol):\n    def __call__(self, data: AnyStr) -> Any:\n        ...\n\n\nclass StreamConsumer(Protocol):\n    @property\n    @abc.abstractmethod\n    def on_start(self) -> StartHandler:\n        ...\n\n    @property\n    @abc.abstractmethod\n    def on_update(self) -> UpdateHandler:\n        ...\n\n    @property\n    @abc.abstractmethod\n    def on_end(self) -> EndHandler:\n        ...\n\n\nclass MessageConsumer(Protocol):\n    @property\n    @abc.abstractmethod\n    def on_message(self) -> MessageHandler:\n        ...\n\n\nclass PyDeviceConsumer(MessageConsumer, StreamConsumer, Protocol):\n    ...\n\n\nclass MetaPyDeviceBackend(abc.ABC):\n    location: str\n\n    @abc.abstractmethod\n    def establish(self, target: str) -> MetaPyDeviceBackend:\n        ...\n\n    @abc.abstractmethod\n    def connect(self) -> None:\n        ...\n\n    @abc.abstractmethod\n    def disconnect(self) -> None:\n        ...\n\n    @abc.abstractmethod\n    def reset(self) -> None:\n        ...\n\n    @abc.abstractmethod\n    def resolve_path(self, target_path: DevicePath | str | Path) -> DevicePath:\n        ...\n\n    @property\n    @abc.abstractmethod\n    def connected(self) -> bool:\n        ...\n\n    @abc.abstractmethod\n    def push_file(\n        self,\n        source_path: HostPath,\n        target_path: DevicePath,\n        *,\n        consumer: PyDeviceConsumer | None,\n        **kwargs,\n    ) -> None:\n        ...\n\n    @abc.abstractmethod\n    def pull_file(\n        self,\n        source_path: DevicePath,\n        target_path: HostPath,\n        *,\n        consumer: PyDeviceConsumer | None,\n        **kwargs,\n    ) -> None:\n        ...\n\n    @abc.abstractmethod\n    def list_dir(self, path: DevicePath) -> list[DevicePath]:\n        ...\n\n    @abc.abstractmethod\n    def remove(self, path: DevicePath) -> None:\n        ...\n\n    @abc.abstractmethod\n    def copy_dir(\n        self,\n        source_path: DevicePath,\n        target_path: HostPath,\n        *,\n        consumer: PyDeviceConsumer | None,\n        **kwargs,\n    ):\n        ...\n\n    @abc.abstractmethod\n    def eval(self, command: str, *, consumer: MessageConsumer | None = None):\n        ...\n\n    @abc.abstractmethod\n    def eval_script(\n        self,\n        contents: AnyStr,\n        target_path: DevicePath | None = None,\n        *,\n        consumer: PyDeviceConsumer | None = None,\n    ):\n        ...\n\n\nAnyBackend = TypeVar(\"AnyBackend\", bound=MetaPyDeviceBackend)\n\n\nclass MetaPyDevice(abc.ABC, Generic[AnyBackend]):\n    pydevice: AnyBackend\n    stream_consumer: StreamConsumer | None\n    message_consumer: MessageConsumer | None\n\n    @abc.abstractmethod\n    def connect(self) -> None:\n        ...\n\n    @abc.abstractmethod\n    def disconnect(self) -> None:\n        ...\n\n    @abc.abstractmethod\n    def copy_to(self, source_path: HostPath, target_path: DevicePath) -> None:\n        ...\n\n    @abc.abstractmethod\n    def copy_from(\n        self, source_path: DevicePath, target_path: HostPath, *, verify_integrity: bool = True\n    ) -> None:\n        ...\n\n    @abc.abstractmethod\n    def remove(self, target_path: DevicePath) -> None:\n        ...\n\n    @abc.abstractmethod\n    def run_script(\n        self, content: AnyStr | StringIO | BytesIO, target_path: DevicePath | None = None\n    ):\n        ...\n"
  },
  {
    "path": "micropy/pyd/backend_rshell.py",
    "content": "from __future__ import annotations\n\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, AnyStr, cast\n\nfrom micropy.exceptions import PyDeviceConnectionError\nfrom micropy.pyd.abc import (\n    DevicePath,\n    HostPath,\n    MessageConsumer,\n    MessageHandler,\n    MetaPyDeviceBackend,\n    PyDeviceConsumer,\n)\n\nif TYPE_CHECKING:\n    from typing import type_check_only  # pragma: no cover\n\n    @type_check_only  # pragma: no cover\n    class RShell:\n        ASCII_XFER: bool\n        QUIET: bool\n\n        def connect(self, port: str):\n            ...\n\n\ntry:\n    import rshell.main as rsh  # type: ignore\n    from rshell.pyboard import Pyboard, PyboardError  # type: ignore\nexcept (\n    ImportError,\n    ModuleNotFoundError,\n):  # pragma: no cover\n    rsh = object()  # type: ignore\n    if TYPE_CHECKING:\n        rsh: RShell = cast(RShell, object())  # type: ignore\n    PyboardError = RuntimeError\n    Pyboard = object()\n\n\nclass RShellConsumer:\n    consumer: MessageHandler\n\n    def __init__(self, child_consumer: MessageHandler):\n        self._outline: list[str] = []\n        self.consumer = child_consumer\n\n    def _output(self, data: str):\n        \"\"\"Yields everything up to a newline.\n\n        Args:\n            data (str): Anything to yield before newline\n\n        \"\"\"\n        if data == \"\\n\":\n            line = \"\".join(self._outline)\n            self._outline = []\n            yield line\n        self._outline.append(data)\n\n    def on_message(self, char: bytes):\n        \"\"\"Pyboard data consumer.\n\n        When a full line of output is detected,\n        it is formatted then logged to stdout\n        and log file.\n\n        Args:\n            char (byte): Byte from PyBoard\n\n        Returns:\n            str: Converted char\n\n        \"\"\"\n        _char = char.decode(\"utf-8\")\n        line = next(self._output(_char), None)\n        if line:\n            self.consumer(line)\n        return char\n\n\nclass RShellPyDeviceBackend(MetaPyDeviceBackend):\n    _connected: bool = False\n    _verbose: bool = False\n    _rsh: rsh\n    _pydevice: Pyboard\n\n    _dev_port: str\n    _repl_active: bool = False\n\n    @property\n    def _pyb_root(self) -> str:\n        \"\"\"pyboard root dirname.\"\"\"\n        if self.connected:\n            dev = rsh.find_serial_device_by_port(self.location)\n            return getattr(dev, \"name_path\", \"/pyboard/\")\n        return \"\"\n\n    @property\n    def connected(self) -> bool:\n        return self._connected\n\n    def resolve_path(self, path: str | DevicePath | Path) -> DevicePath:\n        _path = path\n        if str(path)[0] == \"/\":\n            _path = str(path)[1:]\n        pyb_path = f\"{self._pyb_root}{_path}\"\n        return DevicePath(pyb_path)\n\n    def establish(self, target: str) -> RShellPyDeviceBackend:\n        self._rsh = rsh\n        self._rsh.ASCII_XFER = False\n        self._rsh.QUIET = not self._verbose\n        self.location = target\n        return self\n\n    def connect(self) -> None:\n        try:\n            self._rsh.connect(self.location)\n        except (SystemExit, Exception) as e:\n            raise PyDeviceConnectionError(self.location) from e\n        self._connected = True\n        dev = self._rsh.find_serial_device_by_port(self.location)\n        if dev is None:\n            raise PyDeviceConnectionError(self.location)\n        self._pydevice = dev.pyb\n\n    def disconnect(self) -> None:\n        return\n\n    def reset(self) -> None:\n        return\n\n    def push_file(self, source_path: HostPath, target_path: DevicePath = None, **_) -> None:\n        \"\"\"Copies file to pyboard.\n\n        Args:\n            source_path (str): path to file\n            target_path (str, optional): dest on pyboard. Defaults to None.\n                If None, file is copied to pyboard root.\n\n        Returns:\n            str: path to dest on pyboard\n\n        \"\"\"\n        src_path = Path(source_path).resolve()\n        _dest = target_path or src_path.name\n        dest = self.resolve_path(_dest)\n        self._rsh.cp(str(src_path), str(dest))\n\n    def pull_file(self, source_path: DevicePath, target_path: HostPath, **kwargs) -> None:\n        host_dest = Path(target_path).resolve()\n        device_src = self.resolve_path(source_path)\n        self._rsh.cp(str(device_src), str(host_dest))\n\n    def list_dir(self, path: DevicePath) -> list[DevicePath]:\n        \"\"\"List directory on pyboard.\n\n        Args:\n            path: path to directory\n\n        \"\"\"\n        dir_path = self.resolve_path(path)\n        tree = self._rsh.auto(rsh.listdir, str(dir_path))\n        return tree\n\n    def copy_dir(\n        self, source_path: HostPath | DevicePath, target_path: HostPath | DevicePath, **rsync\n    ):\n        \"\"\"Copy directory from pyboard to machine.\n\n        Args:\n            source_path: path to directory\n            target_path: destination to copy to\n            rsync (dict, optional): additional args to pass to rsync call.\n                Defaults to {}\n\n        \"\"\"\n        dir_path = self.resolve_path(source_path)\n        dest_path = Path(str(target_path))\n        rsync_args = {\n            \"recursed\": True,\n            \"mirror\": False,\n            \"dry_run\": False,\n            \"print_func\": lambda *args: None,\n            \"sync_hidden\": False,\n        }\n        rsync.pop(\"consumer\", None)\n        self._rsh.rsync(dir_path, str(dest_path), **{**rsync_args, **rsync})\n        return dest_path\n\n    @contextmanager\n    def repl(self):\n        \"\"\"Pyboard raw repl context manager.\"\"\"\n        if self._repl_active:\n            yield self._pydevice\n        else:\n            self._pydevice.enter_raw_repl()\n            self._repl_active = True\n            try:\n                yield self._pydevice\n            finally:\n                self._pydevice.exit_raw_repl()\n                self._repl_active = False\n\n    def eval(self, command: str, *, consumer: MessageConsumer = None):\n        \"\"\"Execute bytes on pyboard.\"\"\"\n        _handler = None if consumer is None else RShellConsumer(consumer.on_message).on_message\n        ret, ret_err = self._pydevice.exec_raw(command, data_consumer=_handler)\n        if ret_err:\n            raise PyboardError(\"exception\", ret, ret_err)\n        return ret\n\n    def eval_script(\n        self,\n        contents: AnyStr,\n        target_path: DevicePath | None = None,\n        *,\n        consumer: PyDeviceConsumer = None,\n    ):\n        _contents: str | bytes = contents\n        if isinstance(_contents, bytes):\n            _contents = _contents.decode()\n        with self.repl():\n            try:\n                out_bytes = self.eval(_contents, consumer=consumer)\n            except PyboardError as e:\n                raise Exception(str(e)) from e\n            out = out_bytes.decode(\"utf-8\")\n            return out\n\n    def remove(self, path: DevicePath) -> None:\n        self._rsh.rm(str(path))\n"
  },
  {
    "path": "micropy/pyd/backend_upydevice.py",
    "content": "from __future__ import annotations\n\nimport binascii\nimport hashlib\nimport io\nimport random\nimport stat\nimport string\nimport time\nfrom functools import wraps\nfrom pathlib import Path, PurePosixPath\nfrom typing import AnyStr, Callable, Generator, Optional, TypeVar, Union\n\nimport upydevice\nfrom boltons import iterutils\nfrom micropy.exceptions import PyDeviceConnectionError, PyDeviceError, PyDeviceFileIntegrityError\nfrom rich import print\nfrom typing_extensions import ParamSpec, TypeAlias\nfrom upydevice.phantom import UOS as UPY_UOS\n\nfrom .abc import DevicePath, HostPath, MessageConsumer, MetaPyDeviceBackend, PyDeviceConsumer\nfrom .consumers import NoOpConsumer\n\nAnyUPyDevice: TypeAlias = Union[upydevice.SerialDevice, upydevice.WebSocketDevice]\n\nBUFFER_SIZE = 512\n\nT = TypeVar(\"T\")\nP = ParamSpec(\"P\")\n\n\nclass UOS(UPY_UOS):\n    @upydevice.upy_cmd_c_r()\n    def stat(self, path):\n        return self.dev_dict\n\n\ndef retry(fn: Callable[P, T]) -> Callable[P, T | None]:\n    @wraps(fn)\n    def _wrapper(self_: UPyDeviceBackend, *args: P.args, **kwargs: P.kwargs) -> T | None:\n        _result: T | None = None\n        retry_count = 0\n\n        while retry_count < 4:\n            try:\n                if (integrity := kwargs.pop(\"verify_integrity\", None)) is not None:\n                    # skip integrity check on last retry as last ditch.\n                    kwargs[\"verify_integrity\"] = integrity and retry_count < 3\n                    if integrity and not kwargs[\"verify_integrity\"]:\n                        print(\"Attempting again without file integrity check...\")\n                _result = fn(self_, *args, **kwargs)  # type: ignore\n            except PyDeviceFileIntegrityError as e:\n                retry_count += 1\n                print(e)\n                self_.reset()\n            except Exception as e:\n                retry_count += 1\n                self_.BUFFER_SIZE = BUFFER_SIZE // pow(2, retry_count + 1)\n                print(\"reducing buffer size to:\", self_.BUFFER_SIZE)\n                print(e)\n                self_.reset()\n            else:\n                break\n        return _result\n\n    return _wrapper  # type: ignore\n\n\nclass UPyDeviceBackend(MetaPyDeviceBackend):\n    BUFFER_SIZE: int = BUFFER_SIZE\n\n    _pydevice: AnyUPyDevice\n    _uos: UOS | None = None\n\n    def _ensure_connected(self):\n        if not self.connected:\n            raise PyDeviceError(\"No currently connected device found!\")\n\n    def _rand_device_path(self) -> DevicePath:\n        name = \"\".join(random.sample(string.ascii_lowercase, 6))\n        return self.resolve_path(DevicePath(name))\n\n    @property\n    def uos(self) -> UOS:\n        self._ensure_connected()\n        if not self._uos:\n            self._uos = UOS(self._pydevice)\n        return self._uos\n\n    def _pyb_root(self) -> DevicePath:\n        results = self.uos.stat(\"/flash\")\n        if \"Traceback\" or \"ENOENT\" in results:\n            return DevicePath(\"/\")\n        return DevicePath(\"/flash\")\n\n    def resolve_path(self, path: DevicePath | str | Path) -> DevicePath:\n        _root = PurePosixPath(self._pyb_root())\n        _path = PurePosixPath(path)\n        if _path.is_absolute():\n            if _root == _path or _root in list(_path.parents):\n                return DevicePath(str(_path))\n            _path = _path.relative_to(list(_path.parents)[-1])\n        return DevicePath(str(_root / _path))\n\n    def establish(self, target: str) -> UPyDeviceBackend:\n        self.location = target\n        self._pydevice = upydevice.Device(target, init=True, autodetect=True)\n        return self\n\n    def connect(self):\n        try:\n            self._pydevice.connect()\n        except (SystemExit, Exception) as e:\n            raise PyDeviceConnectionError(self.location) from e\n\n    def disconnect(self):\n        if self.connected:\n            self._pydevice.disconnect()\n\n    def reset(self):\n        self._pydevice.reset()\n        time.sleep(2)\n        self._pydevice.connect()\n        time.sleep(4)\n\n    @property\n    def connected(self) -> bool:\n        return False if getattr(self, \"_pydevice\", None) is None else self._pydevice.connected\n\n    def list_dir(self, path: DevicePath) -> list[DevicePath]:\n        return [DevicePath(p) for p in self.uos.listdir(self.resolve_path(path))]\n\n    def iter_files(self, path: DevicePath) -> Generator[DevicePath, None, None]:\n        path = self.resolve_path(path)\n        self._pydevice.cmd(\"import uos\", silent=True)\n        results = self._pydevice.cmd(f\"list(uos.ilistdir('{path}'))\", silent=True, rtn_resp=True)\n        if not results:\n            return\n        for file_result in results:\n            name, type_, _, _ = file_result\n            abs_path = PurePosixPath(path) / name\n            if type_ == stat.S_IFDIR:\n                yield from self.iter_files(abs_path)\n            else:\n                yield abs_path\n\n    def copy_dir(\n        self,\n        source_path: DevicePath,\n        target_path: HostPath,\n        exclude_integrity: Optional[set[str]] = None,\n        **kwargs,\n    ):\n        target_path = Path(str(target_path))  # type: ignore\n        source_path = self.resolve_path(source_path)\n        exclude_integrity = exclude_integrity or set()\n        for file_path in self.iter_files(source_path):\n            rel_path = PurePosixPath(file_path).relative_to(\n                list(PurePosixPath(file_path).parents)[-1]\n            )\n            # handles os-path conversion\n            file_dest = Path(target_path / rel_path)\n            file_dest.parent.mkdir(parents=True, exist_ok=True)\n            integ_exclude = (\n                file_path in exclude_integrity or Path(file_path).name in exclude_integrity\n            )\n            integrity = kwargs.pop(\"verify_integrity\", True) and not integ_exclude\n            self.pull_file(\n                file_path, HostPath(str(file_dest)), verify_integrity=integrity, **kwargs\n            )\n\n    def push_file(\n        self, source_path: HostPath, target_path: DevicePath, binary: bool = False, **kwargs\n    ) -> None:\n        src_path = Path(str(source_path))\n        src_contents = src_path.read_bytes() if binary else src_path.read_text()\n        self.write_file(src_contents, target_path, **kwargs)\n\n    def pull_file(self, source_path: DevicePath, target_path: HostPath, **kwargs) -> None:\n        src_path = self.resolve_path(source_path)\n        targ_path = Path(str(target_path))\n        source_contents = self.read_file(src_path, **kwargs)\n        if source_contents is None:\n            # TODO: properly report failure to read/copy file.\n            return None\n        targ_path.write_text(source_contents)\n\n    def _iter_hex_chunks(self, content: str):\n        chunked_content = iterutils.chunked_iter(content, self.BUFFER_SIZE)\n        for chunk in chunked_content:\n            hex_chunk = binascii.hexlify(chunk.encode())\n            yield hex_chunk\n\n    @retry\n    def write_file(\n        self,\n        contents: str | bytes,\n        target_path: DevicePath,\n        *,\n        consumer: PyDeviceConsumer = NoOpConsumer,\n    ) -> None:\n        is_bytes = isinstance(contents, bytes)\n        target_path = self.resolve_path(target_path)\n        self._pydevice.cmd(\"import gc\")\n        self._pydevice.cmd(\"import ubinascii\")\n        self._pydevice.cmd(f\"f = open('{str(target_path)}', 'wb')\")\n\n        content_iter = (\n            iterutils.chunked_iter(contents, self.BUFFER_SIZE)\n            if is_bytes\n            else self._iter_hex_chunks(contents)\n        )\n\n        content_size = len(contents)\n        consumer.on_start(name=f\"Writing {str(target_path)}\", size=content_size)\n\n        for chunk in content_iter:\n            cmd = (\n                f\"contents = {chunk}; f.write(contents)\"\n                if is_bytes\n                else f\"contents = ubinascii.unhexlify('{chunk.decode()}'); f.write(contents)\"\n            )\n            self._pydevice.cmd(cmd, silent=True)\n            consumer.on_update(size=len(chunk))\n        consumer.on_end()\n        self._pydevice.cmd(\"f.close()\")\n        self._pydevice.cmd(\"import gc; gc.collect()\")\n\n    def _compute_chunk_size(self) -> int:\n        mem_free = int(\n            self._pydevice.cmd(\"import gc;_=gc.collect();gc.mem_free()\", rtn_resp=True, silent=True)\n        )\n        return min(mem_free // 4, 4096)\n\n    def _compute_device_file_digest(\n        self,\n        device_path: DevicePath,\n        *,\n        chunk_size: int = 256,\n        content_size: Optional[int] = None,\n        pos: int = 0,\n    ) -> str:\n        checksum_cmd = \";\".join(\n            [\n                \"import ubinascii, uhashlib, gc\",\n                \"f=open('{path}', 'rb')\",\n                \"sha = uhashlib.sha256()\",\n                \"__=[sha.update(f.read({chunk_size})) and gc.collect() for _ in range({pos}, {file_size}, {chunk_size})]\",\n                \"ubinascii.hexlify(sha.digest()).decode()\",\n            ]\n        )\n        if content_size is None:\n            content_size = self.uos.stat(str(device_path))[6]\n        sum_cmd = checksum_cmd.format(\n            path=str(device_path), chunk_size=chunk_size, file_size=content_size, pos=pos\n        )\n        return self._pydevice.cmd(sum_cmd, silent=True, rtn_resp=True)\n\n    @retry\n    def read_file(\n        self,\n        target_path: DevicePath,\n        *,\n        consumer: PyDeviceConsumer = NoOpConsumer,\n        verify_integrity: bool = True,\n    ) -> str:\n        target_path = self.resolve_path(target_path)\n\n        read_chunk_cmd = (\n            \"f=open('{path}', 'rb');_=f.seek({pos});ch=f.read({chunk_size});f.close();ch\"\n        )\n\n        content_size = self.uos.stat(str(target_path))[6]\n        buffer = io.BytesIO()\n        pos = 0\n        chunk_size = self._compute_chunk_size()\n        consumer.on_start(\n            name=f\"Reading {Path(target_path).name} (xsize: {chunk_size})\", size=int(content_size)\n        )\n        hasher = hashlib.sha256()\n        while pos < content_size:\n            try:\n                cmd = read_chunk_cmd.format(path=str(target_path), pos=pos, chunk_size=chunk_size)\n                next_chunk = self._pydevice.cmd(cmd, rtn_resp=True, silent=True)\n            except Exception as e:\n                consumer.on_message(f\"Failed to read chunk; retrying ({e})\")\n                self.reset()\n                chunk_size = self._compute_chunk_size()\n                continue\n            if len(next_chunk) == 0:\n                consumer.on_message(\"Failed to read chunk (no data); retrying.\")\n                self.reset()\n                continue\n            hasher.update(next_chunk)\n            buffer.write(next_chunk)\n            pos += chunk_size\n            consumer.on_update(size=len(next_chunk))\n        consumer.on_end()\n\n        if verify_integrity:\n            device_sum = self._compute_device_file_digest(\n                target_path, chunk_size=chunk_size, content_size=content_size, pos=0\n            )\n            digest = hasher.hexdigest()\n            if device_sum != digest:\n                raise PyDeviceFileIntegrityError(\n                    device_path=Path(target_path).name, device_sum=device_sum, digest=digest\n                )\n            consumer.on_message(f\"Verified integrity: {Path(target_path).name}\")\n\n        value = buffer.getvalue().decode()\n        return value\n\n    def eval(self, command: str, *, consumer: MessageConsumer = NoOpConsumer) -> str | None:\n        return self._pydevice.cmd(\n            command, follow=True, pipe=lambda m, *args, **kws: consumer.on_message(m)\n        )\n\n    def eval_script(\n        self,\n        contents: AnyStr,\n        target_path: DevicePath | None = None,\n        *,\n        consumer: PyDeviceConsumer = NoOpConsumer,\n    ):\n        _target_path = (\n            self.resolve_path(target_path) if target_path else f\"{self._rand_device_path()}.py\"\n        )\n        self.write_file(contents, DevicePath(_target_path), consumer=consumer)\n        self.eval(f\"import {Path(_target_path).stem}\", consumer=consumer)\n        self.uos.remove(str(_target_path))\n\n    def remove(self, path: DevicePath) -> None:\n        self.uos.remove(str(path))\n"
  },
  {
    "path": "micropy/pyd/consumers.py",
    "content": "from __future__ import annotations\n\nfrom functools import partialmethod\nfrom typing import Any, Callable, NamedTuple, cast\n\nfrom micropy.pyd.abc import (\n    EndHandler,\n    MessageConsumer,\n    MessageHandler,\n    StartHandler,\n    StreamConsumer,\n    UpdateHandler,\n)\nfrom tqdm import tqdm\n\n\nclass ProgressStreamConsumer:\n    bar: tqdm\n\n    def __init__(\n        self,\n        on_description: Callable[\n            [str, dict[str, Any] | None], tuple[str, dict[str, Any] | None]\n        ] = None,\n        **kwargs,\n    ):\n        self._on_description = on_description or (lambda s, cfg: (s, cfg))\n\n    def on_start(self, *, name: str = None, size: int | None = None):\n        bar_format = \"{l_bar}{bar}| [{n_fmt}/{total_fmt} @ {rate_fmt}]\"\n        tqdm_kwargs = {\n            \"unit_scale\": True,\n            \"unit_divisor\": 1024,\n            \"bar_format\": bar_format,\n        }\n        _name, _tqdm_kws = self._on_description(name or \"\", tqdm_kwargs)\n        # todo: use union operator when min py version is 3.9.\n        tqdm_kwargs.update(_tqdm_kws or dict())\n        self.bar = tqdm(total=size, unit=\"B\", **tqdm_kwargs)\n\n    def on_update(self, *, size: int | None = None):\n        self.bar.update(size)\n\n    def on_end(self):\n        self.bar.close()\n\n\nclass ConsumerDelegate:\n    consumers: list[StreamConsumer | MessageConsumer]\n\n    def __init__(self, *consumers: StreamConsumer | MessageConsumer | None):\n        self.consumers = [i for i in consumers if i]\n\n    def consumer_for(self, action: str, *args, **kwargs):\n        _consumer = next((i for i in self.consumers if hasattr(i, action)), None)\n        if _consumer is None:\n            # default noop\n            return\n        return getattr(_consumer, action)(*args, **kwargs)\n\n    on_message = cast(MessageHandler, partialmethod(consumer_for, \"on_message\"))\n    on_start = cast(StartHandler, partialmethod(consumer_for, \"on_start\"))\n    on_update = cast(UpdateHandler, partialmethod(consumer_for, \"on_update\"))\n    on_end = cast(EndHandler, partialmethod(consumer_for, \"on_end\"))\n\n\nclass StreamHandlers(NamedTuple):\n    on_start: StartHandler\n    on_update: UpdateHandler\n    on_end: EndHandler\n\n\nclass MessageHandlers(NamedTuple):\n    on_message: MessageHandler\n\n\ndef _no_op(*args, **kwargs):\n    return None\n\n\nNoOpStreamConsumer = StreamHandlers(on_start=_no_op, on_update=_no_op, on_end=_no_op)\nNoOpMessageConsumer = MessageHandlers(on_message=_no_op)\nNoOpConsumer = ConsumerDelegate(NoOpMessageConsumer, NoOpMessageConsumer)\n"
  },
  {
    "path": "micropy/pyd/pydevice.py",
    "content": "from __future__ import annotations\n\nfrom io import BytesIO, StringIO\nfrom pathlib import Path\nfrom typing import AnyStr, Generic, Optional, Type\n\nfrom .abc import (\n    AnyBackend,\n    DevicePath,\n    HostPath,\n    MessageConsumer,\n    MetaPyDevice,\n    MetaPyDeviceBackend,\n    StreamConsumer,\n)\nfrom .backend_upydevice import UPyDeviceBackend\nfrom .consumers import ConsumerDelegate\n\n\nclass PyDevice(MetaPyDevice[AnyBackend], Generic[AnyBackend]):\n    pydevice: AnyBackend\n    consumer: ConsumerDelegate\n\n    def __init__(\n        self,\n        location: str,\n        *,\n        backend: Type[MetaPyDeviceBackend] = UPyDeviceBackend,\n        auto_connect: bool = True,\n        stream_consumer: StreamConsumer = None,\n        message_consumer: MessageConsumer = None,\n        delegate_cls: Type[ConsumerDelegate] = ConsumerDelegate,\n    ):\n        self.pydevice = backend().establish(location)\n        self.consumer = delegate_cls(stream_consumer, message_consumer)\n        if auto_connect and self.pydevice:\n            self.pydevice.connect()\n\n    def copy_from(\n        self,\n        source_path: DevicePath,\n        target_path: HostPath,\n        *,\n        verify_integrity: bool = True,\n        exclude_integrity: Optional[set[str]] = None,\n    ) -> None:\n        src_path = Path(str(source_path))\n        # 'is_dir/file' only works on existing paths.\n        if not src_path.suffix:\n            return self.pydevice.copy_dir(\n                DevicePath(source_path),\n                target_path,\n                consumer=self.consumer,\n                verify_integrity=verify_integrity,\n                exclude_integrity=exclude_integrity,\n            )\n        return self.pydevice.pull_file(\n            DevicePath(source_path),\n            target_path,\n            consumer=self.consumer,\n            verify_integrity=verify_integrity,\n        )\n\n    def copy_to(self, source_path: HostPath, target_path: DevicePath, **kwargs) -> None:\n        src_path = Path(str(source_path))\n        host_exists = src_path.exists()\n        if (host_exists and src_path.is_dir()) or (not host_exists and not src_path.suffix):\n            raise RuntimeError(\"Copying dirs to device is not yet supported!\")\n        return self.pydevice.push_file(source_path, target_path, consumer=self.consumer, **kwargs)\n\n    def remove(self, target_path: DevicePath) -> None:\n        return self.pydevice.remove(target_path)\n\n    def connect(self):\n        return self.pydevice.connect()\n\n    def disconnect(self):\n        return self.pydevice.disconnect()\n\n    def run_script(\n        self, content: AnyStr | StringIO | BytesIO, target_path: DevicePath | None = None\n    ):\n        _content = (\n            content\n            if isinstance(\n                content,\n                (\n                    str,\n                    bytes,\n                ),\n            )\n            else content.read()\n        )\n        return self.pydevice.eval_script(_content, target_path, consumer=self.consumer)\n\n    def run(self, content: str) -> str | None:\n        return self.pydevice.eval(content, consumer=self.consumer)\n"
  },
  {
    "path": "micropy/stubs/__init__.py",
    "content": "\"\"\"\nmicropy.stubs\n~~~~~~~~~~~~~~\n\nThis module contains all functionality relating\nto stub files/frozen modules and their usage in MicropyCli\n\"\"\"\n\nfrom . import source\nfrom .manifest import StubsManifest\nfrom .package import AnyStubPackage, StubPackage\nfrom .repo import StubRepository\nfrom .repo_package import StubRepositoryPackage\nfrom .repositories import MicropyStubPackage, MicropythonStubsManifest, MicropythonStubsPackage\nfrom .repository_info import RepositoryInfo\nfrom .stubs import StubManager\n\n__all__ = [\n    \"StubManager\",\n    \"source\",\n    \"StubsManifest\",\n    \"StubPackage\",\n    \"AnyStubPackage\",\n    \"StubRepository\",\n    \"MicropyStubPackage\",\n    \"MicropythonStubsPackage\",\n    \"MicropythonStubsManifest\",\n    \"RepositoryInfo\",\n    \"StubRepositoryPackage\",\n]\n"
  },
  {
    "path": "micropy/stubs/manifest.py",
    "content": "import abc\nfrom typing import FrozenSet, Generic\n\nfrom micropy.stubs.package import AnyStubPackage, StubPackage\nfrom micropy.stubs.repository_info import RepositoryInfo\nfrom pydantic import Field\nfrom pydantic.generics import GenericModel\nfrom typing_extensions import Annotated\n\n\nclass StubsManifest(GenericModel, Generic[AnyStubPackage], abc.ABC):\n    class Config:\n        frozen = True\n\n    repository: RepositoryInfo\n    packages: Annotated[FrozenSet[AnyStubPackage], Field(repr=False)]\n\n    @abc.abstractmethod\n    def resolve_package_url(self, package: StubPackage) -> str:\n        \"\"\"Resolve package to a stub source.\"\"\"\n\n    def resolve_package_absolute_name(self, package: StubPackage) -> str:\n        \"\"\"Resolve package absolute name.\"\"\"\n        return \"/\".join([self.repository.name, package.name])\n\n    def resolve_package_versioned_name(self, package: StubPackage) -> str:\n        \"\"\"Resolve package versioned absolute name.\"\"\"\n        return \"-\".join([package.name, package.version])\n\n    def resolve_package_absolute_versioned_name(self, package: StubPackage) -> str:\n        \"\"\"Resolve package versioned absolute name.\"\"\"\n        return \"-\".join([self.resolve_package_absolute_name(package), package.version])\n"
  },
  {
    "path": "micropy/stubs/package.py",
    "content": "from __future__ import annotations\n\nfrom typing import TypeVar\n\nfrom pydantic import BaseModel\n\n\nclass StubPackage(BaseModel):\n    class Config:\n        frozen = True\n        allow_population_by_field_name = True\n\n    name: str\n    version: str\n\n    @property\n    def package_name(self) -> str:\n        return self.name\n\n\nAnyStubPackage = TypeVar(\"AnyStubPackage\", bound=StubPackage)\n"
  },
  {
    "path": "micropy/stubs/repo.py",
    "content": "from __future__ import annotations\n\nimport collections\nimport inspect\nfrom typing import TYPE_CHECKING, ClassVar, Generator, Iterator, Optional, Type\n\nimport attrs\nimport micropy.exceptions as exc\nfrom boltons.typeutils import get_all_subclasses\n\nfrom .manifest import StubsManifest\nfrom .repo_package import StubRepositoryPackage\n\nif TYPE_CHECKING:\n    from .repository_info import RepositoryInfo\n\n\n@attrs.define\nclass StubRepository:\n    manifests: list[StubsManifest] = attrs.field(factory=list)\n\n    packages_index: collections.ChainMap[str, StubRepositoryPackage] = attrs.field(\n        factory=collections.ChainMap\n    )\n    versions_index: collections.ChainMap[str, list[StubRepositoryPackage]] = attrs.field(\n        factory=collections.ChainMap\n    )\n\n    manifest_types: ClassVar[list[Type[StubsManifest]]] = []\n\n    def __attrs_post_init__(self) -> None:\n        if not any(StubRepository.manifest_types):\n            StubRepository.manifest_types = [\n                klass\n                for klass in get_all_subclasses(StubsManifest)\n                if not inspect.isabstract(klass)\n            ]\n        self.build_indexes()\n\n    @property\n    def packages(self) -> Iterator[StubRepositoryPackage]:\n        \"\"\"Iterate packages in repository.\"\"\"\n        yield from self.packages_index.values()\n\n    def build_indexes(self) -> None:\n        \"\"\"Progressively builds indexes.\"\"\"\n        for manifest in self.manifests:\n            pkg = next(iter(manifest.packages), None)\n            if pkg and manifest.resolve_package_absolute_versioned_name(pkg) in self.packages_index:\n                continue\n            packages_index = dict()\n            versions_index = collections.defaultdict(list)\n            for package in manifest.packages:\n                repo_package = StubRepositoryPackage(manifest=manifest, package=package)\n                packages_index[repo_package.absolute_versioned_name] = repo_package\n                versions_index[repo_package.name].append(repo_package)\n\n            self.packages_index = self.packages_index.new_child(packages_index)\n            self.versions_index = self.versions_index.new_child(dict(versions_index))\n\n    def add_repository(self, info: RepositoryInfo) -> StubRepository:\n        \"\"\"Creates a new `StubRepository` instance with a `StubManifest` derived from `info`.\n\n        Args:\n            info: `RepositoryInfo` instance.\n\n        Returns:\n            `StubRepository` instance.\n\n        \"\"\"\n        contents = info.fetch_source()\n        data = dict(repository=info, packages=contents)\n        for manifest_type in StubRepository.manifest_types:\n            try:\n                manifest = manifest_type.parse_obj(data)\n            except (\n                ValueError,\n                KeyError,\n            ):\n                continue\n            else:\n                return attrs.evolve(\n                    self,\n                    manifests=[*self.manifests, manifest],\n                    packages_index=self.packages_index,\n                    versions_index=self.versions_index,\n                )\n        raise ValueError(f\"Failed to determine manifest format for repo: {info}\")\n\n    def search(\n        self, query: str, include_versions: bool = True\n    ) -> Generator[StubRepositoryPackage, None, None]:\n        \"\"\"Search packages for `query`.\n\n        Args:\n            query: Search constraint.\n            include_versions: Whether to include versions in search results.\n\n        Returns:\n            A generator of `StubRepositoryPackage` objects.\n\n        \"\"\"\n        query = query.strip().lower()\n        for package_name in self.versions_index.keys():\n            if query in package_name.lower() or package_name.lower() in query:\n                if include_versions:\n                    yield from self.versions_index[package_name]\n                    continue\n                yield self.latest_for_package(self.versions_index[package_name][0])\n\n    def latest_for_package(\n        self, repo_package: StubRepositoryPackage\n    ) -> Optional[StubRepositoryPackage]:\n        versions = self.versions_index[repo_package.name]\n        if len(versions) == 1:\n            return versions[0]\n        return max(versions, key=lambda x: x.package.version)\n\n    def resolve_package(self, name: str) -> StubRepositoryPackage:\n        \"\"\"Resolve a package name to a package path.\n\n        Args:\n            name: Package name.\n\n        Returns:\n            Package location.\n\n        Throws:\n            StubNotFound: When package cannot be resolved.\n\n        \"\"\"\n        for package in self.search(str(name)):\n            if package.match_exact(name) or package.match_exact(\n                \"/\".join([package.repo_name, name])\n            ):\n                return package\n            latest = self.latest_for_package(package)\n            if latest and latest.name == name:\n                return latest\n        raise exc.StubNotFound(name)\n"
  },
  {
    "path": "micropy/stubs/repo_package.py",
    "content": "from __future__ import annotations\n\nfrom typing import Iterator\n\nimport attrs\nfrom micropy.stubs import StubPackage, StubsManifest\n\n\n@attrs.frozen\nclass StubRepositoryPackage:\n    manifest: StubsManifest[StubPackage]\n    package: StubPackage\n\n    @property\n    def url(self) -> str:\n        return self.manifest.resolve_package_url(self.package)\n\n    @property\n    def repo_name(self) -> str:\n        return self.manifest.repository.name\n\n    @property\n    def name(self) -> str:\n        return self.package.name\n\n    @property\n    def version(self) -> str:\n        return self.package.version\n\n    @property\n    def absolute_name(self) -> str:\n        return self.manifest.resolve_package_absolute_name(self.package)\n\n    @property\n    def versioned_name(self) -> str:\n        return self.manifest.resolve_package_versioned_name(self.package)\n\n    @property\n    def absolute_versioned_name(self) -> str:\n        return self.manifest.resolve_package_absolute_versioned_name(self.package)\n\n    @property\n    def exact_matchers(self) -> Iterator[str]:\n        yield self.absolute_versioned_name\n        yield self.versioned_name\n        yield self.absolute_name\n\n    @property\n    def partial_matchers(self) -> Iterator[str]:\n        yield from self.exact_matchers\n        yield self.name\n        yield self.version\n\n    def match_exact(self, in_name: str) -> bool:\n        return in_name in self.exact_matchers\n"
  },
  {
    "path": "micropy/stubs/repositories/__init__.py",
    "content": "from .micropy import MicropyStubPackage, MicropyStubsManifest\nfrom .micropython import MicropythonStubsManifest, MicropythonStubsPackage\n\n__all__ = [\n    \"MicropyStubsManifest\",\n    \"MicropyStubPackage\",\n    \"MicropythonStubsPackage\",\n    \"MicropythonStubsManifest\",\n]\n"
  },
  {
    "path": "micropy/stubs/repositories/micropy.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import PurePosixPath\nfrom urllib import parse\n\nfrom pydantic import Field, root_validator\nfrom typing_extensions import Annotated\n\nfrom ..manifest import StubsManifest\nfrom ..package import StubPackage\n\n\nclass MicropyStubPackage(StubPackage):\n    name: str\n    version: Annotated[str, Field(alias=\"sha256sum\")]\n\n\nclass MicropyStubsManifest(StubsManifest[MicropyStubPackage]):\n    location: str\n    path: str\n\n    @root_validator(pre=True)\n    def check(cls, values: dict):\n        pkgs = values[\"packages\"]\n        if \"packages\" in pkgs:\n            values[\"location\"] = pkgs[\"location\"]\n            values[\"path\"] = pkgs[\"path\"]\n            values[\"packages\"] = pkgs[\"packages\"]\n        return values\n\n    def resolve_package_url(self, package: StubPackage) -> str:\n        base_path = PurePosixPath(parse.urlparse(self.location).path)\n        pkg_path = base_path / PurePosixPath(self.path) / PurePosixPath(package.name)\n        url = parse.urljoin(self.location, str(pkg_path))\n        return url\n"
  },
  {
    "path": "micropy/stubs/repositories/micropython.py",
    "content": "from __future__ import annotations\n\nimport functools\nfrom typing import TYPE_CHECKING\n\nfrom distlib.locators import locate\nfrom distlib.version import NormalizedVersion\nfrom pydantic import Field, validator\nfrom typing_extensions import Annotated\n\nfrom ..manifest import StubsManifest\nfrom ..package import StubPackage\n\nif TYPE_CHECKING:\n    from distlib.database import Distribution\n\n\n@functools.total_ordering\nclass MicropythonStubsPackage(StubPackage):\n    name: str\n    version: Annotated[str, Field(alias=\"pkg_version\")]\n\n    @property\n    def package_version(self) -> NormalizedVersion:\n        return NormalizedVersion(self.version)\n\n    def __lt__(self, other: MicropythonStubsPackage) -> bool:\n        return self.package_version < other.package_version\n\n    def __eq__(self, other: MicropythonStubsPackage) -> bool:\n        return self.name == other.name and self.version == other.version\n\n\nclass MicropythonStubsManifest(StubsManifest[MicropythonStubsPackage]):\n    @validator(\"packages\", pre=True)\n    def _get_packages(cls, v: dict[str, dict]):\n        data = v[\"data\"].values()\n        return list(data)\n\n    def resolve_package_url(self, package: StubPackage) -> str:\n        dist: Distribution = locate(f\"{package.name} ({package.version})\")\n        dist_url = next(i for i in dist.download_urls if \"tar.gz\" in i)\n        return dist_url\n"
  },
  {
    "path": "micropy/stubs/repository_info.py",
    "content": "from __future__ import annotations\n\nfrom datetime import timedelta\nfrom typing import Any\n\nimport requests\nfrom cachier import cachier\nfrom pydantic import BaseModel, HttpUrl\n\n\nclass RepositoryInfo(BaseModel):\n    name: str\n    display_name: str\n    source: HttpUrl\n\n    class Config:\n        frozen = True\n\n    @cachier(stale_after=timedelta(days=1), next_time=True)\n    def fetch_source(self) -> dict[str, Any]:\n        return requests.get(self.source).json()\n"
  },
  {
    "path": "micropy/stubs/source.py",
    "content": "\"\"\"\nmicropy.stubs.source\n~~~~~~~~~~~~~~\n\nThis module contains abstractions for handling stub sources\nand their location.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport abc\nimport shutil\nimport tempfile\nfrom contextlib import ExitStack, contextmanager\nfrom functools import partial, reduce\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Callable, ContextManager, Optional, Protocol, Union, cast\n\nimport attrs\nimport micropy.exceptions as exc\nfrom micropy import utils\nfrom micropy.logger import Log\nfrom micropy.utils.types import PathStr\n\nif TYPE_CHECKING:\n    from micropy.stubs.repo import StubRepository\n\n\nclass LocateStrategy(Protocol):\n    @abc.abstractmethod\n    def prepare(self, location: PathStr) -> Union[PathStr, tuple[PathStr, Callable[..., Any]]]:\n        ...\n\n\nlogger = Log.add_logger(__name__, show_title=False)\n\n\n@attrs.define\nclass StubSource:\n    \"\"\"Handles sourcing stubs.\"\"\"\n\n    locators: list[LocateStrategy] = attrs.field()\n    location: Optional[PathStr] = attrs.field(default=None)\n\n    @locators.default\n    def _default_locators(self: StubSource) -> list[LocateStrategy]:\n        return [RemoteStubLocator(), StubInfoSpecLocator()]\n\n    def _do_locate(self, stack: ExitStack, path: PathStr, locator: LocateStrategy) -> PathStr:\n        logger.debug(f\"running (strategy:{locator}) @ (location:{path})\")\n        response = locator.prepare(path)\n        parts = iter(response if isinstance(response, tuple) else (response,))\n        path = next(parts, path)\n        teardown = next(parts, None)\n        if teardown:\n            logger.debug(f\"adding teardown callback for: {locator}\")\n            stack.callback(teardown)\n        logger.debug(f\"results of (strategy:{locator}) -> (location:{path})\")\n        return path\n\n    @contextmanager\n    def ready(self, location: Optional[PathStr] = None) -> ContextManager[PathStr]:\n        \"\"\"Yields prepared Stub Source.\n\n        Allows StubSource subclasses to have a preparation\n        method before providing a local path to itself.\n\n        Yields:\n            Resolved PathLike object to stub source\n\n        \"\"\"\n        with ExitStack() as stack:\n            reducer = cast(\n                Callable[[PathStr, LocateStrategy], PathStr], partial(self._do_locate, stack)\n            )\n            path = reduce(reducer, self.locators, location or self.location)\n            yield path\n\n\n@attrs.define\nclass StubInfoSpecLocator(LocateStrategy):\n    def prepare(self, location: PathStr) -> PathStr:\n        info_path = next(Path(location).rglob(\"info.json\"), None)\n        return location if info_path is None else info_path.parent\n\n\n@attrs.define\nclass RemoteStubLocator(LocateStrategy):\n    \"\"\"Stub Source for remote locations.\"\"\"\n\n    def _unpack_archive(self, file_bytes: bytes, path: PathStr) -> PathStr:\n        \"\"\"Unpack archive from bytes buffer.\n\n        Args:\n            file_bytes (bytes): Byte array to extract from\n                Must be from tarfile with gzip compression\n            path (str): path to extract file to\n\n        Returns:\n            path: path extracted to\n\n        \"\"\"\n        path = Path(utils.extract_tarbytes(file_bytes, path))\n        output = next(path.iterdir())\n        return output\n\n    def prepare(self, location: PathStr) -> tuple[PathStr, Optional[Callable[..., Any]]] | PathStr:\n        \"\"\"Retrieves and unpacks source.\n\n        Prepares remote stub resource by downloading and\n        unpacking it into a temporary directory.\n        This directory is removed via the returned teardown.\n\n        \"\"\"\n        if not utils.is_url(location):\n            logger.debug(f\"{self}: {location} not viable, skipping...\")\n            return location\n        tmp_dir = tempfile.mkdtemp()\n        tmp_path = Path(tmp_dir)\n        filename = utils.get_url_filename(location).split(\".tar.gz\")[0]\n        _file_name = \"\".join(logger.iter_formatted(f\"$B[{filename}]\"))\n        content = utils.stream_download(location, desc=f\"{logger.get_service()} {_file_name}\")\n        source_path = self._unpack_archive(content, tmp_path)\n        teardown = partial(shutil.rmtree, tmp_path)\n        return source_path, teardown\n\n\n@attrs.define\nclass RepoStubLocator(LocateStrategy):\n    repo: StubRepository = attrs.field(repr=False)\n\n    def prepare(self, location: PathStr) -> Union[PathStr, tuple[PathStr, Callable[..., Any]]]:\n        if not self.repo:\n            return location\n        try:\n            source = self.repo.resolve_package(location)\n        except exc.StubNotFound as e:\n            logger.debug(f\"{self}: {location} not found in repo, skipping... (exc: {e})\")\n            return location\n        else:\n            return source.url\n\n\ndef get_source(location, **kwargs):\n    \"\"\"Factory for StubSource Instance.\n\n    Deprecated. Todo: Remove.\n\n    Args:\n        location (str): PathLike object or valid URL\n\n    Returns:\n        obj: Either Local or Remote StubSource Instance\n\n    \"\"\"\n    return StubSource(**kwargs, location=location)\n"
  },
  {
    "path": "micropy/stubs/stubs.py",
    "content": "from __future__ import annotations\n\nimport json\nimport shutil\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom distlib import locators, metadata\nfrom micropy import data, utils\nfrom micropy.exceptions import StubError, StubValidationError\nfrom micropy.logger import Log\nfrom micropy.stubs import source\nfrom packaging.utils import parse_sdist_filename\n\nif TYPE_CHECKING:\n    from micropy.stubs.repo import StubRepository\n\n\nclass StubManager:\n    \"\"\"Manages a collection of Stubs.\n\n    Kwargs:\n        resource (str): Default resource path\n        repos ([StubRepo]): Repos for Remote Stubs\n\n    Raises:\n        StubError: a stub is missing a def file\n        StubValidationError: a stubs def file is not valid\n\n    Returns:\n        object: Instance of StubManager\n\n    \"\"\"\n\n    repo: StubRepository\n\n    _schema = data.SCHEMAS / \"stubs.json\"\n    _firm_schema = data.SCHEMAS / \"firmware.json\"\n\n    def __init__(self, resource=None, repos=None):\n        self._loaded = set()\n        self._firmware = set()\n        self.resource = resource\n        self.repo = repos\n        self.log = Log.add_logger(\"Stubs\", stdout=False, show_title=False)\n        if self.resource:\n            self.load_from(resource, strict=False)\n\n    def __iter__(self):\n        return iter(self._loaded)\n\n    def __len__(self):\n        return len(self._loaded)\n\n    def iter_by_firmware(self, stubs=None):\n        \"\"\"Iterate stubs sorted by firmware.\n\n        Args:\n            stubs ([Stub], optional): Sublist of Stubs to iterate over.\n                Defaults to None. If none, uses all installed stubs.\n\n        \"\"\"\n        loaded = stubs or self._loaded\n        for firm in self._firmware:\n            stubs = [s for s in loaded if s.firmware == firm]\n            yield (firm, stubs)\n        other = [s for s in loaded if s.firmware is None]\n        yield (\"Unknown\", other)\n\n    def verbose_log(self, state):\n        \"\"\"Enable Stub logging to stdout.\n\n        Args:\n            state (bool): State to set\n\n        Returns:\n            bool: state\n\n        \"\"\"\n        self.log.stdout = state\n        return state\n\n    def _load(self, stub_source, strict=True, **kwargs):\n        \"\"\"Loads a stub into StubManager.\n\n        Args:\n            stub_source (StubSource): Stub Source Instance\n            strict (bool, optional): Raise Exception if stub fails to resolve.\n                Defaults to True.\n\n        Raises:\n            e: Exception raised by resolving failure\n\n        Returns:\n            Stub: Instance of Stub\n\n        \"\"\"\n        with stub_source.ready() as src_path:\n            if not self.is_valid(src_path):\n                self.log.debug(\"attempting to load stub from metadata.\")\n                try:\n                    infos = self.from_metadata(\n                        parse_sdist_filename(stub_source.location.split(\"/\")[-1])[0], src_path\n                    )\n                    kwargs[\"name\"] = infos[\"name\"]\n                except Exception as e:\n                    self.log.debug(f\"failed to load from metadata: {e}\")\n            try:\n                stub_type = self._get_stubtype(src_path)\n            except Exception as e:\n                self.log.debug(f\"{Path(src_path).name} failed to validate: {e}\")\n                if strict:\n                    raise e\n            else:\n                if stub_type is FirmwareStub:\n                    fware = stub_type(src_path, **kwargs)\n                    self._firmware.add(fware)\n                    self.log.debug(f\"Firmware Loaded: {fware}\")\n                    return fware\n                stub = stub_type(src_path, **kwargs)\n                fware = self.resolve_firmware(stub)\n                stub.firmware = fware\n                self._loaded.add(stub)\n                self.log.debug(f\"Loaded: {stub}\")\n                return stub\n\n    def resolve_firmware(self, stub):\n        \"\"\"Resolves FirmwareStub for DeviceStub instance.\n\n        Args:\n            stub (DeviceStub): Stub to resolve\n\n        Returns:\n            FirmwareStub: Instance of FirmwareStub\n            NoneType: None if an appropriate\n                FirmwareStub cannot be found\n\n        \"\"\"\n        fware_name = stub.firmware_name\n        self.log.info(f\"Detected Firmware: $[{fware_name}]\")\n        results = (f for f in self._firmware if f.firmware == fware_name)\n        fware = next(results, None)\n        if not fware:\n            try:\n                self.log.info(\"Firmware not found locally, attempting to install it...\")\n                fware = self.add(fware_name)\n            except Exception:\n                self.log.error(\"Failed to resolve firmware!\")\n                return None\n            else:\n                self.log.success(f\"{fware_name} firmware added!\")\n                return fware\n        return fware\n\n    def validate(self, path, schema=None):\n        \"\"\"Validates given stub path against its schema.\n\n        Args:\n            path (str): path to validate\n            schema (str, optional): Path to schema. Defaults to None.\n                If None, the DeviceStub schema is used.\n\n        Raises:\n            StubError: Raised if no info file can be found\n            StubValidationError: Raised if the info file fails validation\n\n        \"\"\"\n        self.log.debug(f\"Validating: {path}\")\n        schema = schema or self._schema\n        path = Path(path).resolve()\n        stub_info = path / \"info.json\"\n        val = utils.Validator(schema)\n        try:\n            val.validate(stub_info)\n        except FileNotFoundError as e:\n            self.log.error(f\"missing info spec @ {path}\", exception=e)\n            raise StubError(f\"{path.name} contains no info file!\") from e\n        except Exception as e:\n            self.log.error(f\"validation error at {path}\", exception=e)\n            raise StubValidationError(path, str(e)) from e\n\n    def _get_stubtype(self, path):\n        \"\"\"Resolves appropriate stub type.\n\n        Args:\n            path (str): path to stub\n\n        Returns:\n            cls: Appropriate class for stub\n\n        \"\"\"\n        try:\n            self.validate(path)\n        except StubValidationError:\n            try:\n                self.validate(path, schema=self._firm_schema)\n            except Exception as e:\n                raise e\n            else:\n                return FirmwareStub\n        except Exception as e:\n            raise e\n        else:\n            return DeviceStub\n\n    def is_valid(self, path):\n        \"\"\"Check if stub is valid without raising an exception.\n\n        Args:\n            path (str): path to stub\n\n        Returns:\n            bool: True if stub is valid\n\n        \"\"\"\n        try:\n            self._get_stubtype(path)\n        except Exception:\n            return False\n        else:\n            return True\n\n    def _check_existing(self, location):\n        \"\"\"check if location is or contains an existing stub.\n\n        Args:\n            location (str): name or path of Stub\n\n        Returns:\n            generator of existing stubs\n\n        \"\"\"\n        try:\n            do_recurse = self._should_recurse(location)\n        except StubError:\n            yield\n        else:\n            if do_recurse:\n                for s in (self._check_existing(p) for p in location.iterdir()):\n                    yield next(s, None)\n            path_name = Path(location).name\n            stub = next(\n                (\n                    s\n                    for s in self._loaded\n                    if any(t in (s.name, s.path.name) for t in (path_name, location))\n                ),\n                None,\n            )\n            if stub:\n                yield stub\n\n    def load_from(self, directory, *args, **kwargs):\n        \"\"\"Recursively loads stubs from a directory.\n\n        Args:\n            directory (str): Path to load from\n\n        Returns:\n            [DeviceStub]: List of loaded Stubs\n\n        \"\"\"\n        dir_path = Path(str(directory)).resolve()\n        dirs = dir_path.iterdir()\n        sources = [source.StubSource([source.StubInfoSpecLocator()], location=d) for d in dirs]\n        stubs = []\n        for stub in sources.copy():\n            if self.is_valid(stub.location):\n                stub_type = self._get_stubtype(stub.location)\n                if stub_type is FirmwareStub:\n                    sources.remove(stub)\n                    self._load(stub, *args, **kwargs)\n        stubs.extend([self._load(s, *args, **kwargs) for s in sources])\n        return stubs\n\n    def _should_recurse(self, location):\n        \"\"\"Checks for multiple stubs in a location.\n\n        Args:\n            location (str): location of potential stub\n\n        Raises:\n            StubError: No info files could be found\n\n        Returns:\n            bool: True if multiple stubs are found\n\n        \"\"\"\n        if not Path(location).exists():\n            return False\n        path = Path(location).resolve()\n        info_glob = list(path.rglob(\"info.json\"))\n        if len(info_glob) == 0:\n            raise StubError(f\"{path.name} contains no info file!\")\n        if len(info_glob) > 1:\n            return True\n        return False\n\n    def add(self, location, dest=None, force=False):\n        \"\"\"Add stub(s) from source.\n\n        Args:\n            source (str): path to stub(s)\n            dest (str, optional): path to copy stubs to.\n                Defaults to self.resource\n            force (bool, optional): overwrite existing stubs.\n                Defaults to False.\n\n        Raises:\n            TypeError: No resource or destination provided\n\n        \"\"\"\n        _dest = dest or self.resource\n        if not _dest:\n            raise TypeError(\"No Stub Destination Provided!\")\n        dest = Path(str(_dest)).resolve()\n        stubs = [s for s in self._check_existing(location) if s is not None]\n        if any(stubs):\n            for stub in stubs:\n                if not force:\n                    self.log.info(f\"$[{stub}] is already installed!\")\n                    return stub\n                self.log.info(f\"Uninstalling $[{stub.name}]...\")\n                shutil.rmtree(stub.path)\n        if self._should_recurse(location):\n            return self.load_from(location, strict=False, copy_to=dest)\n        self.log.info(\"\\nResolving stub...\")\n        stub_source = source.StubSource(\n            [\n                source.RepoStubLocator(self.repo),\n                source.RemoteStubLocator(),\n                source.StubInfoSpecLocator(),\n            ],\n            location,\n        )\n        return self._load(stub_source, copy_to=dest)\n\n    def from_stubber(self, path, dest):\n        \"\"\"Formats stubs generated by createstubs.py.\n\n        Creates a stub package from the stubs generated by\n        createstubs.py. Also attempts to auto-resolve the stubs\n        firmware name.\n\n        Args:\n            path (str): path to generated stubs\n            dest (str): path to output\n\n        Returns:\n            str: formatted stubs\n\n        \"\"\"\n        _path = Path(path).resolve()\n        dest = Path(dest).resolve()\n        mod_file = next(_path.rglob(\"modules.json\"))\n        path = mod_file.parent\n        mod_data = json.load(mod_file.open())\n        dev_fware = mod_data[\"firmware\"]\n        fname = dev_fware.get(\"name\", None)\n        out_name = f\"{dev_fware['sysname']}\"\n        # TODO: Attempt to Autoresolve Firmware name and add it to info.json\n        if fname:\n            out_name = f\"{out_name}-{fname}\"\n        out_name = f\"{out_name}-{dev_fware['version']}\"\n        out_stub = dest / out_name\n        info_file = out_stub / \"info.json\"\n        stub_path = out_stub / \"stubs\"\n        out_stub.mkdir(exist_ok=True, parents=True)\n        json.dump(mod_data, info_file.open(\"w+\"))\n        shutil.copytree(path, stub_path)\n        return out_stub\n\n    def from_metadata(self, package_name: str, path: Path) -> dict[str, str]:\n        \"\"\"Creates stub info.json meta from dist metadata.\n\n        Notes:\n            This method is a (very) dirty adapter (just like above...)\n            until this module can be rewritten from scratch.\n\n        \"\"\"\n        metadatas = (metadata.Metadata(path=p) for p in path.rglob(\"PKG-INFO\"))\n        meta = next(m for m in metadatas if m.todict()[\"name\"] == package_name)\n        info_path = path / \"info.json\"\n        name_parts = set(meta.todict()[\"name\"].split(\"-\"))\n        name_parts.remove(\"stubs\")\n        # oh lawd, look away!!\n        dev_name = min(name_parts, key=lambda s: len(s))\n        name_parts.remove(dev_name)\n        firm_name = name_parts.pop()\n        firm = {\n            \"ver\": meta.version or \"\",\n            \"port\": dev_name,\n            \"arch\": \"\",\n            \"sysname\": dev_name,\n            \"name\": firm_name,\n            \"mpy\": 0,\n            \"version\": meta.version or \"\",\n            \"machine\": \"\",\n            \"build\": \"\",\n            \"nodename\": dev_name,\n            \"platform\": dev_name,\n            \"family\": \"\",\n        }\n        info_json = {\n            \"firmware\": firm,\n            \"stubber\": {\"version\": meta.version or \"\"},\n            \"modules\": [],\n            \"name\": package_name,\n        }\n        info_path.write_text(json.dumps(info_json))\n        # TODO: properly resolve requirements prior/external to this.\n        requires = (r for r in meta.run_requires if \"stub\" in r)\n        for req in requires:\n            dist = locators.locate(req)\n            if not dist:\n                continue\n            dist_url = next((i for i in dist.download_urls if \".tar.gz\" in i), None)\n            if dist_url:\n                self.add(dist_url)\n        return info_json\n\n    def resolve_subresource(self, stubs, subresource):\n        \"\"\"Resolve or Create StubManager from list of stubs.\n\n        Args:\n            stubs ([Stub]): List of stubs to use in subresource\n            subresource (str): path to subresource\n\n        Returns:\n            StubManager: StubManager with subresource stubs\n\n        \"\"\"\n        for stub in stubs:\n            fware = stub.firmware\n            if fware:\n                link = subresource / fware.path.name\n                fware = FirmwareStub.resolve_link(fware, link)\n            link = subresource / stub.path.name\n            stub = DeviceStub.resolve_link(stub, link)\n            stub.firmware = fware\n            yield stub\n\n\nclass Stub:\n    \"\"\"Abstract Parent for Stub Related Classes.\n\n    Not Meant to be instantiated directly. Contains common logic\n    between different types of Stubs (ex. Firmware vs Device)\n\n    Raises:\n        NotImplementedError: name property is not implemented\n\n    Returns:\n        Instance of Stub\n\n    \"\"\"\n\n    def __init__(self, path, copy_to=None, **kwargs):\n        self.path = Path(path)\n        ref = self.path / \"info.json\"\n        self.info = json.load(ref.open())\n        if copy_to is not None:\n            self.copy_to(copy_to)\n\n    def find_root(self, path: Path) -> Path:\n        \"\"\"Attempt to find appropriate stub root.\"\"\"\n        pyi_files = path.rglob(\"*.pyi\")\n        if pyi_path := next(pyi_files, None):\n            return pyi_path.parent\n        return path\n\n    def copy_to(self, dest, name=None):\n        \"\"\"Copy stub to a directory.\"\"\"\n        if not name:\n            dest = Path(dest) / self.path.name\n        shutil.copytree(self.path, dest)\n        self.path = dest.resolve()\n        return self\n\n    @classmethod\n    def resolve_link(cls, stub, link_path):\n        \"\"\"Resolve or Create Stub Symlink.\n\n        Args:\n            stub (Stub): stub to resolve\n            link_path (str): path to link\n\n        Returns:\n            Stub: Stub from symlink\n\n        \"\"\"\n        fware = stub.firmware\n        if utils.is_dir_link(link_path):\n            return cls(link_path, firmware=fware)\n        utils.create_dir_link(link_path, stub.path)\n        return cls(link_path, firmware=fware)\n\n    @property\n    def name(self):\n        \"\"\"Human friendly stub name.\"\"\"\n        raise NotImplementedError\n\n    def __eq__(self, other):\n        return self.name == getattr(other, \"name\", None)\n\n    def __hash__(self):\n        return hash(self.name)\n\n    def __str__(self):\n        return self.name\n\n\nclass DeviceStub(Stub):\n    \"\"\"Handles Device Specific Stubs.\n\n    Args:\n        path (str): path to stub\n        copy_to (str, optional): Path to copy Stub to. Defaults to None.\n\n    Returns:\n        Device Stub Instance\n\n    \"\"\"\n\n    def __init__(self, path, copy_to=None, **kwargs):\n        super().__init__(path, copy_to, **kwargs)\n\n        stubs_path = self.path / \"stubs\"\n        self.stubs = self.find_root(stubs_path if stubs_path.exists() else self.path)\n\n        frozen_path = self.path / \"frozen\"\n        self.frozen = self.find_root(frozen_path if frozen_path.exists() else self.path)\n\n        stubber = self.info.get(\"stubber\")\n        self.stub_version = stubber.get(\"version\")\n\n        self.firm_info = self.info.get(\"firmware\")\n        self.firmware = kwargs.get(\"firmware\", None)\n        self.sysname = self.firm_info.get(\"sysname\")\n        self.version = self.firm_info.get(\"version\")\n        # TODO: make this module not garbage.\n        self._name = kwargs.get(\"name\", self.info.get(\"name\", None))\n\n    @property\n    def firmware_name(self):\n        \"\"\"Return an appropriate firmware name.\n\n        Returns:\n            str: Name of Firmware\n\n        \"\"\"\n        if isinstance(self.firmware, FirmwareStub):\n            return self.firmware.firmware\n        fware = self.firm_info.get(\"name\", None)\n        if not fware:\n            fware = self.firm_info.get(\"firmware\", \"\").strip()\n            fware.replace(\" \", \"-\")\n        return fware\n\n    @property\n    def name(self):\n        if self._name:\n            return self._name\n        if not isinstance(self.firmware, FirmwareStub):\n            return f\"{self.sysname}-{self.version}\"\n        return f\"{self.sysname}-{self.firmware_name}-{self.version}\"\n\n    def __repr__(self):\n        return (\n            f\"DeviceStub(sysname={self.sysname}, firmware=\"\n            f\"{self.firmware_name}, version={self.version}, \"\n            f\"path={self.path})\"\n        )\n\n\nclass FirmwareStub(Stub):\n    \"\"\"Handles Firmware Specific Modules.\n\n    Args:\n        path (str): path to stub\n        copy_to (str, optional): Path to copy Stub to. Defaults to None.\n\n    Returns:\n        FirmwareStub Instance\n\n    \"\"\"\n\n    def __init__(self, path, copy_to=None, **kwargs):\n        super().__init__(path, copy_to=copy_to, **kwargs)\n\n        self.frozen = self.path / \"frozen\"\n        self.repo = self.info.get(\"repo\")\n        firmware = self.info.get(\"firmware\").strip()\n        self.firmware = firmware.replace(\" \", \"-\")\n\n    @property\n    def name(self):\n        return self.firmware\n\n    def __repr__(self):\n        return f\"FirmwareStub(firmware={self.firmware}, repo={self.repo})\"\n"
  },
  {
    "path": "micropy/utils/__init__.py",
    "content": "\"\"\"\nmicropy.utils\n~~~~~~~~~~~~~~\n\nThis module provides utility functions that are used within\nMicropyCli.\n\"\"\"\n\nfrom .decorators import lazy_property\nfrom .helpers import (\n    create_dir_link,\n    ensure_existing_dir,\n    ensure_valid_url,\n    extract_tarbytes,\n    get_cached_data,\n    get_class_that_defined_method,\n    get_package_meta,\n    get_url_filename,\n    is_dir_link,\n    is_downloadable,\n    is_existing_dir,\n    is_update_available,\n    is_url,\n    iter_requirements,\n    search_xml,\n    stream_download,\n)\nfrom .stub import generate_stub\nfrom .validate import Validator\n"
  },
  {
    "path": "micropy/utils/_compat.py",
    "content": "from __future__ import annotations\n\ntry:\n    from importlib import metadata as _metadata\nexcept ImportError:\n    # compat for py <3.10\n    import importlib_metadata as metadata\nelse:\n    # workaround for\n    # https://github.com/python/mypy/issues/1393\n    metadata = _metadata\n\n__all__ = [\"metadata\"]\n"
  },
  {
    "path": "micropy/utils/decorators.py",
    "content": "\"\"\"\nmicropy.utils.decorators\n~~~~~~~~~~~~~~\n\nThis module contains generic decorators\nused by MicropyCli\n\"\"\"\n\n__all__ = [\"lazy_property\"]\n\n\ndef lazy_property(fn):\n    attr = \"_lazy__\" + fn.__name__\n\n    @property\n    def _lazy_property(self):\n        if not hasattr(self, attr):\n            setattr(self, attr, fn(self))\n        return getattr(self, attr)\n\n    return _lazy_property\n"
  },
  {
    "path": "micropy/utils/helpers.py",
    "content": "\"\"\"\nmicropy.utils.helpers\n~~~~~~~~~~~~~~\n\nThis module contains generic utility helpers\nused by MicropyCli\n\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport io\nimport os\nimport shutil\nimport subprocess as subproc\nimport sys\nimport tarfile\nimport xml.etree.ElementTree as ET\nfrom datetime import timedelta\nfrom pathlib import Path\nfrom typing import Iterable, Optional, Union\n\nimport requests\nimport requirements\nfrom cachier import cachier\nfrom packaging import version\nfrom requests import exceptions as reqexc\nfrom requests import utils as requtil\nfrom tqdm import tqdm\n\nfrom ._compat import metadata\nfrom .types import PathStr\n\n__all__ = [\n    \"is_url\",\n    \"get_url_filename\",\n    \"ensure_existing_dir\",\n    \"ensure_valid_url\",\n    \"is_downloadable\",\n    \"is_existing_dir\",\n    \"stream_download\",\n    \"search_xml\",\n    \"get_package_meta\",\n    \"extract_tarbytes\",\n    \"iter_requirements\",\n    \"create_dir_link\",\n    \"is_dir_link\",\n    \"is_update_available\",\n    \"get_cached_data\",\n    \"get_class_that_defined_method\",\n]\n\n\ndef is_url(url):\n    \"\"\"Check if provided string is a url.\n\n    Args:\n        url (str): url to check\n\n    Returns:\n        bool: True if arg url is a valid url\n\n    \"\"\"\n    scheme = requtil.urlparse(str(url)).scheme\n    return scheme in (\n        \"http\",\n        \"https\",\n    )\n\n\n@cachier(stale_after=timedelta(days=1))\ndef ensure_valid_url(url):\n    \"\"\"Ensure a url is valid.\n\n    Args:\n        url (str): URL to validate\n\n    Raises:\n        InvalidURL: URL is not a valid url\n        ConnectionError: Failed to connect to url\n        HTTPError: Response was not 200 <OK>\n\n    Returns:\n        str: valid url\n\n    \"\"\"\n    if not is_url(url):\n        raise reqexc.InvalidURL(f\"{url} is not a valid url!\")\n    resp = requests.head(url, allow_redirects=True)\n    resp.raise_for_status()\n    return url\n\n\ndef ensure_existing_dir(path: PathStr) -> Path:\n    \"\"\"Ensure path exists and is a directory.\n\n    If path does exist, it will be returned as\n    a pathlib.PurePath object\n\n    Args:\n        path (str): path to validate and return\n\n    Raises:\n        NotADirectoryError: path does not exist\n        NotADirectoryError: path is not a directory\n\n    Returns:\n        object: pathlib.PurePath object\n\n    \"\"\"\n    _path = Path(path)\n    path = _path.absolute()\n    try:\n        if not path.exists():\n            raise NotADirectoryError(f\"{_path} does not exist!\")\n        if not path.is_dir():\n            raise NotADirectoryError(f\"{_path} is not a directory!\")\n    except OSError as e:\n        raise NotADirectoryError(f\"{_path} is not a valid path!\") from e\n    return _path\n\n\ndef is_existing_dir(path):\n    \"\"\"Check if path is an existing directory.\n\n    Args:\n        path (str): path to check\n\n    Returns:\n        bool: True if path exists and is a directory\n\n    \"\"\"\n    try:\n        ensure_existing_dir(path)\n    except NotADirectoryError:\n        return False\n    else:\n        return True\n\n\ndef is_downloadable(url):\n    \"\"\"Checks if the url can be downloaded from.\n\n    Args:\n        url (str): url to check\n\n    Returns:\n        bool: True if contains a downloadable resource\n\n    \"\"\"\n    try:\n        ensure_valid_url(url)\n    except Exception:\n        return False\n    headers = requests.head(url).headers\n    content_type = headers.get(\"content-type\").lower()\n    ctype = content_type.split(\"/\")\n    if any(\n        t\n        in (\n            \"text\",\n            \"html\",\n        )\n        for t in ctype\n    ):\n        return False\n    return True\n\n\ndef get_url_filename(url):\n    \"\"\"Parse filename from url.\n\n    Args:\n        url (str): url to parse\n\n    Returns:\n        str: filename of url\n\n    \"\"\"\n    path = requtil.urlparse(url).path\n    file_name = Path(path).name\n    return file_name\n\n\ndef stream_download(url, **kwargs):\n    \"\"\"Stream download with tqdm progress bar.\n\n    Args:\n        url (str): url to file\n\n    Returns:\n        bytearray: bytearray of content\n\n    \"\"\"\n    stream = requests.get(url, stream=True)\n    content = bytearray()\n    total_size = int(stream.headers.get(\"content-length\", len(stream.content)))\n    block_size = 32 * 1024\n    bar_format = \"{l_bar}{bar}| [{n_fmt}/{total_fmt} @ {rate_fmt}]\"\n    tqdm_kwargs = {\n        \"unit_scale\": True,\n        \"unit_divisor\": 1024,\n        \"smoothing\": 0.1,\n        \"bar_format\": bar_format,\n    }\n    tqdm_kwargs.update(kwargs)\n    with tqdm(total=total_size, unit=\"B\", **tqdm_kwargs) as pbar:\n        for block in stream.iter_content(block_size):\n            pbar.update(len(block))\n            content.extend(block)\n    return content\n\n\n@cachier(stale_after=timedelta(days=3))\ndef search_xml(url, node):\n    \"\"\"Search xml from url by node.\n\n    Args:\n        url (str): url to xml\n        node (str): node to search for\n\n    Returns:\n        [str]: matching nodes\n\n    \"\"\"\n    resp = requests.get(url)\n    xml = resp.content.decode(\"UTF-8\")\n    root = ET.fromstring(xml)\n    root_ns = root.tag[1 : root.tag.find(\"}\")]\n    namespace = {\"ns\": root_ns}\n    _results = root.findall(f\"./*/ns:{node}\", namespace)\n    results = [k.text for k in _results]\n    return results\n\n\ndef iter_requirements(path):\n    \"\"\"Iterate requirements from a requirements.txt file.\n\n    Args:\n        path (str): path to file\n\n    \"\"\"\n    req_path = Path(path).absolute()\n    with req_path.open(\"r\") as rfile:\n        yield from requirements.parse(rfile)\n\n\ndef get_package_meta(name, url):\n    \"\"\"Retrieve package metadata from PyPi.\n\n    Args:\n        name (str): Name of package with specs.\n        url (str): Url to package.\n\n    Returns:\n        dict: Dictionary of Metadata\n\n    \"\"\"\n\n    def _iter_compare(in_val, comp_to, operator):\n        for t in comp_to:\n            state = eval(f\"in_val {operator} t\")\n            if state:\n                yield t\n\n    resp = requests.get(url)\n    data = resp.json()\n    pkg = next(requirements.parse(name))\n    releases = data[\"releases\"]\n    # Latest version\n    spec_data = list(releases.items())[-1][1]\n    if pkg.specs:\n        spec_comp, spec_v = pkg.specs[0]\n        spec_v = version.parse(spec_v)\n        rel_versions = [version.parse(k) for k in releases.keys()]\n        spec_key = str(next(_iter_compare(spec_v, rel_versions, spec_comp)))\n        spec_data = releases[spec_key]\n    # Find .tar.gz meta\n    tar_meta = next(i for i in spec_data if \".tar.gz\" in Path(i[\"url\"]).name)\n    return tar_meta\n\n\ndef is_within_directory(\n    directory: Union[os.PathLike, str], target: Union[str, os.PathLike]\n) -> bool:\n    abs_directory = os.path.abspath(directory)\n    abs_target = os.path.abspath(target)\n\n    prefix = os.path.commonprefix([abs_directory, abs_target])\n\n    return prefix == abs_directory\n\n\ndef safe_extract(\n    tar: tarfile.TarFile,\n    path: Union[os.PathLike, str] = \".\",\n    members: Optional[Iterable[tarfile.TarInfo]] = None,\n    *,\n    numeric_owner: bool = False,\n) -> None:\n    for member in tar.getmembers():\n        member_path = os.path.join(path, member.name)\n        if not is_within_directory(path, member_path):\n            raise Exception(\"Attempted Path Traversal in Tar File\")\n\n    tar.extractall(path, members, numeric_owner=numeric_owner)\n\n\ndef extract_tarbytes(file_bytes: bytes, path: str) -> str:\n    \"\"\"Extract tarfile as bytes.\n\n    Args:\n        file_bytes (bytearray): Bytes of file to extract\n        path (str): Path to extract it to\n\n    Returns:\n        path: destination path\n\n    \"\"\"\n    tar_bytes_obj = io.BytesIO(file_bytes)\n    with tarfile.open(fileobj=tar_bytes_obj, mode=\"r:gz\") as tar:\n        safe_extract(tar, path)\n    return path\n\n\ndef create_dir_link(source, target):\n    \"\"\"Creates a platform appropriate directory link.\n\n    On POSIX systems it will create a symlink.\n    On Windows it will fallback on a directory junction if needed\n\n    Args:\n        source (os.Pathlike): Path to create link at.\n        target (os.Pathlike): Path to link to.\n\n    Raises:\n        OSError: Symlink Creation Failed\n        OSError: Symlink and Directory Junction Fallback Failed\n\n    \"\"\"\n    platform = sys.platform\n    source = Path(source)\n    target = Path(target)\n    try:\n        source.symlink_to(target, target_is_directory=True)\n    except OSError as e:\n        # Handle non-admin/non-dev windows links\n        if not platform == \"win32\":\n            # handles exFAT disk format (links not working)\n            if e.errno == 38:\n                shutil.copytree(\n                    str(target.absolute()), str(source.absolute()), symlinks=False, ignore=None\n                )\n                return\n            elif e.errno == 17:  # folder exists\n                return\n            else:\n                raise e\n\n        # Fall back to directory junction\n        cmd = [\"MKLINK\", \"/J\", str(source.absolute()), str(target.absolute())]\n        exit_code = subproc.call(cmd, shell=True, stdout=subproc.PIPE, stderr=subproc.PIPE)\n        if exit_code:\n            raise e\n\n\ndef is_dir_link(path):\n    \"\"\"Test if path is either a symlink or directory junction.\n\n    Args:\n        path (os.Pathlike): Path to test.\n\n    Returns:\n        bool: True if path is a type of link.\n\n    \"\"\"\n    platform = sys.platform\n    path = Path(path)\n    if path.is_symlink():\n        return True\n    if platform == \"win32\":\n        # Test for Directory Junction\n        resolved = str(path.resolve())\n        if not str(path.absolute()) == resolved:\n            return True\n    return False\n\n\ndef is_update_available():\n    \"\"\"Check if micropy-cli update is available.\n\n    Returns:\n        bool: True if update available, else False.\n\n    \"\"\"\n    url = \"https://pypi.org/pypi/micropy-cli/json\"\n    data = get_cached_data(url)\n    versions = [k for k in data[\"releases\"].keys() if \"rc\" not in k]\n    if versions:\n        latest = version.parse(versions[-1])\n        cur_version = version.parse(metadata.version(\"micropy-cli\"))\n        if cur_version < latest:\n            return str(latest)\n    return False\n\n\n@cachier(stale_after=timedelta(days=3), next_time=True)\ndef get_cached_data(url):\n    \"\"\"Wrap requests with a short cache.\"\"\"\n    source_data = requests.get(url).json()\n    return source_data\n\n\ndef get_class_that_defined_method(meth):\n    \"\"\"Determines Class that defined a given method.\n\n    See - https://stackoverflow.com/a/25959545\n\n    Args:\n        meth (Callable): Method to determine class from\n\n    Returns:\n        Callable: Class that defined method\n\n    \"\"\"\n    if inspect.ismethod(meth):\n        for cls in inspect.getmro(meth.__self__.__class__):\n            if cls.__dict__.get(meth.__name__) is meth:\n                return cls\n        meth = meth.__func__  # fallback to __qualname__ parsing\n    if inspect.isfunction(meth):\n        cls = getattr(\n            inspect.getmodule(meth), meth.__qualname__.split(\".<locals>\", 1)[0].rsplit(\".\", 1)[0]\n        )\n        if isinstance(cls, type):\n            return cls\n    return getattr(meth, \"__objclass__\", None)  # handle special descriptor objects\n"
  },
  {
    "path": "micropy/utils/stub.py",
    "content": "\"\"\"Micropy stub utils.\"\"\"\nfrom __future__ import annotations\n\nimport importlib.util\nimport io\nimport sys\nfrom pathlib import Path\nfrom types import ModuleType\nfrom typing import Optional\n\nimport libcst as cst\nimport libcst.codemod as codemod\nimport micropy.data\nfrom stubber import minify\nfrom stubber.codemod import board as stub_board\nfrom stubber.utils import stubmaker\n\n\ndef locate_create_stubs() -> Path:\n    \"\"\"Locate createstubs.py\"\"\"\n    return Path(importlib.util.find_spec(\"stubber.board.createstubs\").origin)\n\n\ndef import_source_code(module_name: str, path: Path) -> ModuleType:\n    \"\"\"Dynamically create and load module from python source code.\n\n    Args:\n        module_name: name of new module to create.\n        path: path to source code.\n\n    Returns:\n        Dynamically created module.\n\n    \"\"\"\n    spec = importlib.util.spec_from_file_location(module_name, path)\n    module = importlib.util.module_from_spec(spec)\n    sys.modules[module_name] = module\n    spec.loader.exec_module(module)\n    return module\n\n\ndef import_stubber() -> ModuleType:\n    \"\"\"Dynamically import stubber.\n\n    We do this because `micropython-stubs` is not a python package, so\n    we can't import from it as you would normally.\n\n    \"\"\"\n    vers_path = micropy.data.STUBBER / \"src\" / \"version.py\"\n    src_path = micropy.data.STUBBER / \"src\" / \"utils.py\"\n    # stubber utils expects an ambiguous 'version' import\n    import_source_code(\"version\", vers_path)\n    mod = import_source_code(\"stubber.utils\", src_path)\n    return mod\n\n\ndef generate_stub(path, log_func=None):\n    \"\"\"Create Stub from local .py file.\n\n    Args:\n        path (str): Path to file\n        log_func (func, optional): Callback function for logging.\n            Defaults to None.\n\n    Returns:\n        tuple: Tuple of file path and generated stub path.\n\n    \"\"\"\n    if stubmaker is None:\n        raise ImportError(\"micropython-stubber requires a python version of >= 3.8\")\n    stubmaker.STUBGEN_OPT.quiet = True\n    file_path = Path(path).absolute()\n    stubbed_path = file_path.with_suffix(\".pyi\")\n    stubmaker.generate_pyi_from_file(file_path)\n    # ensure stubs reside next to their source.\n    result = next((file_path.parent.rglob(f\"**/{stubbed_path.name}\")), stubbed_path)\n    if result.exists():\n        result.replace(stubbed_path)\n    if not any(result.parent.iterdir()):\n        result.parent.rmdir()\n    files = (file_path, stubbed_path)\n    return files\n\n\ndef prepare_create_stubs(\n    *,\n    variant: Optional[stub_board.CreateStubsVariant] = None,\n    modules_set: Optional[stub_board.ListChangeSet] = None,\n    problem_set: Optional[stub_board.ListChangeSet] = None,\n    exclude_set: Optional[stub_board.ListChangeSet] = None,\n    compile: bool = False,\n) -> io.StringIO | io.BytesIO:\n    if stub_board is None:\n        raise ImportError(\"micropython-stubber requires a python version of >= 3.8\")\n    variant = variant or stub_board.CreateStubsVariant.BASE\n    ctx = codemod.CodemodContext()\n    code_mod = stub_board.CreateStubsCodemod(\n        ctx, variant=variant, modules=modules_set, problematic=problem_set, excluded=exclude_set\n    )\n    create_stubs = cst.parse_module(locate_create_stubs().read_text())\n    result = code_mod.transform_module_impl(create_stubs).code\n    result_io = io.StringIO(result)\n    minified_io = io.StringIO()\n    minify.minify(result_io, minified_io, keep_report=True, diff=False)\n    minified_io.seek(0)\n    # TODO: compile w/ mpy-cross\n    if compile:\n        compiled_io = io.BytesIO()\n        minify.cross_compile(minified_io, compiled_io)\n        compiled_io.seek(0)\n        return compiled_io\n    return minified_io\n"
  },
  {
    "path": "micropy/utils/types.py",
    "content": "\"\"\"Type utilities and variables.\"\"\"\n\nfrom __future__ import annotations\n\nfrom os import PathLike\nfrom typing import Any, Protocol, Union, runtime_checkable\n\nfrom typing_extensions import TypeAlias\n\n# PathLike string or string type alias.\nPathStr: TypeAlias = Union[str, PathLike]\n\n\n@runtime_checkable\nclass SupportsLessThan(Protocol):\n    def __lt__(self, other: Any) -> bool:\n        ...\n"
  },
  {
    "path": "micropy/utils/validate.py",
    "content": "\"\"\"\nmicropy.utils.validate\n~~~~~~~~~~~~~~\n\nThis module contains utility functions for MicropyCli\nthat center on data validation\n\"\"\"\n\nimport json\nfrom pathlib import Path\n\nfrom jsonschema import Draft7Validator\n\n\nclass Validator:\n    \"\"\" \"jsonschema wrapper for file validation.\n\n    Returns:\n        object: Validator Instance\n\n    \"\"\"\n\n    def __init__(self, schema_path):\n        schema = self._load_json(schema_path)\n        self.schema = Draft7Validator(schema)\n\n    def _load_json(self, path):\n        \"\"\"Loads json data from file.\n\n        Args:\n            path (str): path to file\n\n        Returns:\n            Loaded JSON data as an array or dict\n\n        \"\"\"\n        file = Path(path).resolve()\n        data = json.load(file.open())\n        return data\n\n    def validate(self, path):\n        \"\"\"Validates json file against a schema.\n\n        Args:\n            path (str): path to json file to validate\n\n        Returns:\n            jsonschema.validate\n\n        \"\"\"\n        data = self._load_json(path)\n        return self.schema.validate(data)\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"micropy-cli\"\nversion = \"4.2.2\"\ndescription = \"Micropython Project Management Tool with VSCode support, Linting, Intellisense, Dependency Management, and more!\"\nauthors = [\"Braden Mars <bradenmars@bradenmars.me>\"]\nlicense = \"MIT\"\npackages = [{ include = \"micropy\" }]\ninclude = [\n    { path = \"tests\", format = \"sdist\" },\n    \"README.md\",\n    \"CHANGELOG.md\",\n    \"LICENSE\",\n    \"micropy/lib/stubber/src/*\",\n    \"micropy/lib/stubber/board/*\",\n    \"micropy/lib/stubber/process.py\",\n    \"micropy/lib/stubber/minified/*\"\n]\nexclude = [\"micropy/lib/stubber/*\"]\nreadme = \"README.md\"\nhomepage = \"https://github.com/BradenM/micropy-cli\"\nrepository = \"https://github.com/BradenM/micropy-cli\"\ndocumentation = \"https://micropy-cli.readthedocs.io\"\nkeywords = [\"micropython\", \"stubs\", \"linting\", \"vscode\", \"intellisense\"]\nclassifiers = [\n    \"Operating System :: OS Independent\",\n    \"Topic :: Software Development :: Code Generators\",\n    \"Topic :: Software Development :: Embedded Systems\",\n    \"Topic :: Software Development :: Build Tools\",\n    \"Programming Language :: Python :: Implementation :: MicroPython\",\n    \"Programming Language :: Python :: Implementation :: CPython\",\n    \"Development Status :: 5 - Production/Stable\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n]\n\n[tool.poetry.scripts]\nmicropy = 'micropy.app:app'\n\n[tool.poetry.dependencies]\npython = \">=3.9,<3.12\"\nboltons = \"==23.1.1\"\ncachier = \"2.3.0\"\nJinja2 = \"3.1.4\"\nquestionary = \"1.10.0\"\nrequests = \"2.32.2\"\nrequirements-parser = \"0.9.0\"\ntqdm = \"4.66.4\"\nclick = \"8.1.7\"\ncolorama = { version = \"==0.4.6\", platform = \"win32\" }\njsonschema = \"=3.2.0\"\ndpath = \"==1.5.0\"\nGitPython = \"==3.1.43\"\npackaging = \"==21.3\"\npython-minifier = \"==2.9.0\"\nmypy = \"^1\"\n## rshell is broken on windows (with python >=3.10) due to pyreadline dependency.\nrshell = [\n    { version = \"^0.0.31\", platform = \"win32\", python = \"<3.10\" },\n    { version = \"^0.0.31\", platform = \"!=win32\" },\n]\nMarkupSafe = \"==2.1.5\"\nupydevice = \"0.3.8\"\nattrs = \"==23.2.0\"\ntyping-extensions = \"4.12.2\"\npydantic = \"1.10.16\"\ndistlib = \"0.3.8\"\nimportlib-metadata = { version = \"==5.2.0\", python = '<3.10' }\nmicropython-stubber = \"==1.23.2\"\nlibcst = \">=1.1.0,<2.0.0\"\ntyper = {version = \"0.12.3\", extras = [\"all\"]}\n\n[tool.poetry.group.dev.dependencies]\nbump2version = \"^0.5.11\"\npre-commit = \"^3.0.2\"\nmypy = { version = \"^1.0\", extras = [\"dmypy\"] }\ntypes-requests = \"^2.27.14\"\npylint = \"^2.7\"\nbetter-exceptions = \"^0.3.3\"\npdbpp = {version = \"^0.10.3\", platform = \"!=win32\" }\n\n[tool.poetry.group.lint]\noptional = true\n\n[tool.poetry.group.lint.dependencies]\npyupgrade = \"^3.3.1\"\nisort = \"^5.12.0\"\nblack = \">=22.10,<23.0\"\nruff = \"^0.5.0\"\n\n[tool.poetry.group.test]\noptional = true\n\n[tool.poetry.group.test.dependencies]\npytest = \"^7.1.1\"\npytest-cov = \"^4.0\"\npytest-datadir = \"^1.3\"\npytest-mock = \"^3.5\"\npytest-runner = \"^6.0\"\npytest-testmon = \"^1.3\"\npytest-watch = \"^4.2\"\npytest-xdist = \"^3.0\"\npytest-forked = \"^1.3\"\npytest-randomly = \"^3.7.0\"\npytest-sugar = \"^0.9.4\"\nrequests-mock = \"^1.9\"\nflaky = \"^3.7.0\"\ncoveralls = \"^3.0\"\ncodacy-coverage = \"^1.3\"\nmock = \"^5.0.1\"\n\n[tool.poetry.group.docs]\noptional = true\n\n[tool.poetry.group.docs.dependencies]\nsphinx-click = \"^4.3.0\"\nsphinx-autodoc-typehints = \"^1.19.2\"\nrecommonmark = \"^0.7.0\"\nSphinx = \"^5.2.1\"\nsphinx-rtd-theme = \"^1.0.0\"\n\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[tool.black]\nline-length = 100\nskip-string-normalization = false\ntarget-version = ['py38']\ninclude = '\\.pyi?$'\nexclude = '''\n\n(\n  /(\n      \\.eggs         # exclude a few common directories in the\n    | \\.git          # root of the project\n    | \\.hg\n    | \\.mypy_cache\n    | \\.tox\n    | \\.venv\n    | _build\n    | build\n    | dist\n    | micropy/lib\n  )/\n  | foo.py           # also separately exclude a file named foo.py in\n                     # the root of the project\n)\n'''\n\n[tool.isort]\nprofile = \"black\"\nsrc_paths = [\"src\"]\nline_length = 100\nhonor_noqa = true\n\n[tool.ruff]\nselect = [\n    \"E\",  # pycodestyle errors\n    \"W\",  # pycodestyle warnings\n    \"F\",  # pyflakes\n    # \"I\",  # isort\n    # \"D\",  # pydocstyle\n    \"UP\", # pyupgrade\n    \"C\",  # flake8-comprehensions\n    \"B\",  # flake8-bugbear\n    # \"PT\",  # flake8-pytest-style\n    \"RUF\" # ruff specific rules\n]\nignore = [\n    \"E501\", # line too long - let black handle.\n    \"C901\", # too complex\n    \"C408\", # rewrite as literal; makes comprehensions harder sometimes imo\n    \"B008\", # no function call as arg default; typer uses this pattern.\n]\nline-length = 100\ntarget-version = 'py38'\nsrc = ['micropy']\nextend-exclude = ['micropy/lib', 'tests/test_stubs', 'tests/data', 'micropy/utils/__init__.py', 'micropy/__init__.py']\n\n[tool.ruff.pyupgrade]\nkeep-runtime-typing = true\n\n[tool.pytest.ini_options]\ncollect_ignore = ['setup.py', 'micropy/lib/*', 'micropy/lib/**/*']\ntestpaths = \"tests\"\naddopts = \"--maxprocesses=4 -ra -q\"\nnorecursedirs = \"micropy/lib\"\nmock_use_standalone_module = true\n\n[tool.coverage.run]\nsource = [\"micropy\"]\nomit = [\"micropy/project/template/*\", \"micropy/lib/*\", \"micropy/lib/**/*\"]\n\n[tool.coverage.report]\nexclude_lines = [\n    \"if __name__ == .__main__.:\",\n    'class .*\\bProtocol\\):',\n    'raise NotImplementedError',\n    'raise AssertionError',\n    'def __repr__',\n    'noqa',\n    '@(abc\\.)?abstractmethod',\n    'pragma: no cover'\n]\n\n[tool.mypy]\nplugins = [\n  \"pydantic.mypy\"\n]\nfollow_imports = \"silent\"\nwarn_redundant_casts = true\nwarn_unused_ignores = true\ndisallow_any_generics = true\ncheck_untyped_defs = true\nno_implicit_reexport = true\ndisallow_untyped_defs = true\n\n\n[[tool.mypy.overrides]]\nmodule = [\"boltons\", \"upydevice\", \"upydevice.*\", \"rshell\", \"tqdm\"]\nignore_missing_imports = true\n\n\n[tool.pydantic-mypy]\ninit_forbid_extra = true\ninit_typed = true\nwarn_required_dynamic_aliases = true\nwarn_untyped_fields = true\n"
  },
  {
    "path": "release-please-config.json",
    "content": "{\n    \"$schema\": \"https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json\",\n    \"release-type\": \"python\",\n    \"prerelease\": true,\n    \"include-v-in-tag\": true,\n    \"bootstrap-sha\": \"fbdf74f0cb3a4621fa7373495c9b2e7153009818\",\n    \"changelog-sections\": [\n        {\n            \"type\": \"feat\",\n            \"section\": \"Features\"\n        },\n        {\n            \"type\": \"feature\",\n            \"section\": \"Features\"\n        },\n        {\n            \"type\": \"fix\",\n            \"section\": \"Bug Fixes\"\n        },\n        {\n            \"type\": \"perf\",\n            \"section\": \"Performance Improvements\"\n        },\n        {\n            \"type\": \"revert\",\n            \"section\": \"Reverts\"\n        },\n        {\n            \"type\": \"docs\",\n            \"section\": \"Documentation\",\n            \"hidden\": false\n        },\n        {\n            \"type\": \"style\",\n            \"section\": \"Styles\",\n            \"hidden\": true\n        },\n        {\n            \"type\": \"chore\",\n            \"section\": \"Miscellaneous Chores\",\n            \"hidden\": true\n        },\n        {\n            \"type\": \"refactor\",\n            \"section\": \"Code Refactoring\",\n            \"hidden\": false\n        },\n        {\n            \"type\": \"test\",\n            \"section\": \"Tests\",\n            \"hidden\": true\n        },\n        {\n            \"type\": \"build\",\n            \"section\": \"Build System\",\n            \"hidden\": true\n        },\n        {\n            \"type\": \"ci\",\n            \"section\": \"Continuous Integration\",\n            \"hidden\": true\n        }\n    ],\n    \"plugins\": [\n        {\n            \"type\": \"sentence-case\"\n        }\n    ],\n    \"packages\": {\n        \".\": {}\n    }\n}\n"
  },
  {
    "path": "scripts/export-docs-reqs.sh",
    "content": "#!/usr/bin/env bash\n\nROOT=\"$(git rev-parse --show-toplevel)\"\n\npoetry plugin add poetry-export-plugin>/dev/null\n\npoetry export --with docs --with dev --without-hashes -f requirements.txt -o \"${ROOT}/docs/requirements.txt\"\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/app/conftest.py",
    "content": "from typing import NamedTuple\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom micropy import MicroPy, logger\nfrom micropy.project import Project\nfrom micropy.stubs import StubManager, StubRepository\nfrom pytest_mock import MockFixture\nfrom typer.testing import CliRunner\n\n\n@pytest.fixture()\ndef runner():\n    runner = CliRunner()\n    with runner.isolated_filesystem():\n        yield runner\n\n\nclass MicroPyScenario(NamedTuple):\n    project_exists: bool = True\n    has_stubs: bool = True\n    impl_add: bool = True\n\n\n@pytest.fixture()\ndef mock_repo(mocker: MockFixture) -> StubRepository:\n    mock = mocker.MagicMock(StubRepository, autospec=True)\n    mocker.patch(\"micropy.stubs.StubRepository\", return_value=mock)\n    return mock\n\n\n@pytest.fixture()\ndef micropy_obj(\n    request: pytest.FixtureRequest, tmp_path, mocker: MockFixture, mock_repo\n) -> MicroPy:\n    mpy = mocker.MagicMock(MicroPy, autospec=True)\n    mpy.log = logger.Log.add_logger(\"MicroPy\")\n    mpy.project = mocker.MagicMock(Project, autospec=True).return_value\n    mpy.stubs = mocker.MagicMock(StubManager, autospec=True).return_value\n    mpy.repo = mock_repo\n    if param := getattr(request, \"param\", MicroPyScenario()):\n        if param.impl_add:\n            mpy.stubs.add = lambda i: i\n        mpy.project.exists = param.project_exists\n        stubs = [\"a-stub\"] if param.has_stubs else []\n        mpy.stubs.__iter__.return_value = iter(stubs)\n    mocker.patch(\"micropy.main.MicroPy\", return_value=mpy)\n    return mpy\n\n\n@pytest.fixture(params=[True, False])\ndef context_mock(request: pytest.FixtureRequest, mocker: MockFixture, micropy_obj) -> MagicMock:\n    ctx = mocker.MagicMock()\n    ctx.ensure_object.return_value = micropy_obj\n    ctx.find_object.return_value = micropy_obj\n    ctx.resilient_parsing = request.param\n    ctx.obj = micropy_obj\n    return ctx\n"
  },
  {
    "path": "tests/app/test_main.py",
    "content": "from pathlib import Path\nfrom typing import List, NamedTuple, Optional\n\nimport pytest\nimport typer\nfrom micropy import utils\nfrom micropy.app import main as main_app\nfrom micropy.app.main import TemplateEnum, app\nfrom micropy.project import Project\nfrom pytest_mock import MockFixture\nfrom tests.app.conftest import MicroPyScenario, context_mock\n\n# current working directory symbol\nCWD = object()\n\n\n@pytest.fixture\ndef project_path(tmp_path):\n    return tmp_path / \"test_project\"\n\n\nclass TemplateParamCase(NamedTuple):\n    value: Optional[List[TemplateEnum]]\n    expected: List[TemplateEnum]\n    prompt: Optional[List[TemplateEnum]]\n    confirm: Optional[bool] = None\n\n\n@pytest.fixture(\n    params=[\n        TemplateParamCase(value=None, expected=[], prompt=[], confirm=True),\n        TemplateParamCase(value=None, expected=[TemplateEnum.vscode], prompt=[TemplateEnum.vscode]),\n        TemplateParamCase(value=[TemplateEnum.vscode], expected=[TemplateEnum.vscode], prompt=None),\n    ]\n)\ndef template_param(request: pytest.FixtureRequest, mocker: MockFixture):\n    param: TemplateParamCase = request.param\n    mock_prompt = mocker.MagicMock()\n    mock_confirm = mocker.MagicMock(return_value=param.prompt)\n    mock_prompt.ask.return_value = param.prompt\n    mock_confirm.ask.return_value = param.confirm\n    mocker.patch(\"questionary.checkbox\", return_value=mock_prompt)\n    mocker.patch(\"questionary.confirm\", return_value=mock_confirm)\n    ctx = request.getfixturevalue(\"context_mock\")\n    yield param\n    if ctx.resilient_parsing:\n        return\n    if param.prompt is None:\n        mock_prompt.ask.assert_not_called()\n    else:\n        mock_prompt.ask.assert_called_once()\n    if param.confirm is None:\n        mock_confirm.ask.assert_not_called()\n    else:\n        mock_confirm.ask.assert_called_once()\n\n\ndef test_template_callback(mocker: MockFixture, micropy_obj, context_mock, template_param):\n    if context_mock.resilient_parsing:\n        assert main_app.template_callback(context_mock, template_param.value) is None\n        return\n    assert main_app.template_callback(context_mock, template_param.value) == template_param.expected\n\n\n@pytest.mark.parametrize(\n    \"input,expected\",\n    [\n        (None, CWD),\n        (\"NewProject\", \"NewProject\"),\n        (\"/tmp/NewProject\", \"/tmp/NewProject\"),\n    ],\n)\ndef test_path_callback(mocker: MockFixture, micropy_obj, context_mock, input, expected):\n    expected = Path.cwd() if expected == CWD else expected\n    if context_mock.resilient_parsing:\n        assert main_app.path_callback(context_mock, input) is None\n        return\n    assert main_app.path_callback(context_mock, input) == expected\n\n\n@pytest.mark.parametrize(\n    \"input,expected\",\n    [\n        (None, CWD),\n        (\"NewProject\", \"NewProject\"),\n        (\"/tmp/NewProject\", \"/tmp/NewProject\"),\n    ],\n)\ndef test_name_callback(mocker: MockFixture, micropy_obj, context_mock, input, expected):\n    expected = (Path.cwd()).name if expected == CWD else expected\n    mock_ask = mocker.MagicMock()\n    mock_ask.ask.return_value.strip.return_value = expected\n    mocker.patch(\"questionary.text\", return_value=mock_ask)\n    if context_mock.resilient_parsing:\n        assert main_app.name_callback(context_mock, input) is None\n        return\n    assert main_app.name_callback(context_mock, input) == expected\n\n\n@pytest.mark.parametrize(\n    \"micropy_obj,expect\",\n    [\n        (\n            MicroPyScenario(),\n            True,\n        ),\n        (MicroPyScenario(project_exists=False), None),\n    ],\n    indirect=[\"micropy_obj\"],\n)\ndef test_main_callback(mocker: MockFixture, context_mock, micropy_obj, expect):\n    if context_mock.resilient_parsing:\n        assert main_app.main_callback(context_mock) is None\n        return\n    util_mock = mocker.patch.object(utils, \"is_update_available\", return_value=False)\n    main_app.main_callback(context_mock)\n    if expect:\n        util_mock.assert_called_once()\n    else:\n        util_mock.assert_not_called()\n\n\n@pytest.mark.parametrize(\n    \"input,prompt,micropy_obj,expected\",\n    [\n        (None, [], MicroPyScenario(has_stubs=False), typer.Abort),\n        (None, [], MicroPyScenario(), typer.BadParameter),\n        ([\"a-stub\"], None, MicroPyScenario(), [\"a-stub\"]),\n        ([\"a-stub\"], None, MicroPyScenario(has_stubs=False), [\"a-stub\"]),\n        (None, [\"some-stub\"], MicroPyScenario(), [\"some-stub\"]),\n    ],\n    indirect=[\"micropy_obj\"],\n)\ndef test_stubs_callback(mocker: MockFixture, context_mock, input, prompt, micropy_obj, expected):\n    mock_ask = mocker.MagicMock()\n    mock_ask.ask.return_value = prompt\n    mocker.patch(\"questionary.checkbox\", return_value=mock_ask)\n    if context_mock.resilient_parsing:\n        assert main_app.stubs_callback(context_mock, input) is None\n        return\n    if not isinstance(expected, list):\n        with pytest.raises(expected):\n            main_app.stubs_callback(context_mock, input)\n    else:\n        assert main_app.stubs_callback(context_mock, input) == expected\n        if prompt is None:\n            mock_ask.ask.assert_not_called()\n        else:\n            mock_ask.ask.assert_called_once()\n\n\n# Test main_init function\ndef test_main_init(mocker, micropy_obj, project_path, runner):\n    ask_mock = mocker.MagicMock()\n    ask_mock.ask.return_value = \"test_project\"\n    mocker.patch(\"questionary.text\", return_value=ask_mock)\n    mocker.patch(\"questionary.checkbox\")\n\n    result = runner.invoke(app, [\"init\", str(project_path)], obj=micropy_obj)\n    print(result.stdout)\n    assert result.exit_code == 0\n    assert f\"Initiating {project_path.name}\" in result.stdout\n    assert \"Project Created\" in result.stdout\n\n\nctx = context_mock\n\n\n@pytest.mark.parametrize(\n    \"micropy_obj,expect\",\n    [\n        (MicroPyScenario(), Project),\n        (MicroPyScenario(project_exists=False), typer.Abort),\n    ],\n    indirect=[\"micropy_obj\"],\n)\ndef test_ensure_project(mocker: MockFixture, micropy_obj, expect, tmp_path):\n    ctx = mocker.MagicMock(typer.Context, autospec=True).return_value\n    ctx.ensure_object.return_value = micropy_obj\n    if expect == typer.Abort:\n        with pytest.raises(typer.Abort):\n            main_app.ensure_project(ctx)\n    else:\n        project = main_app.ensure_project(ctx)\n        assert project == micropy_obj.project\n\n\n@pytest.mark.parametrize(\n    \"input,expected\",\n    [(None, None), (Path(\"somepath\"), typer.Exit)],\n)\ndef test_install_local_callback(micropy_obj, context_mock, input, expected):\n    if context_mock.resilient_parsing:\n        assert main_app.install_local_callback(context_mock, input) is None\n        return\n    if expected == typer.Exit:\n        with pytest.raises(typer.Exit):\n            main_app.install_local_callback(context_mock, input)\n        micropy_obj.project.add_package.assert_called_once()\n        return\n    assert main_app.install_local_callback(context_mock, input) == expected\n    micropy_obj.project.add_package.assert_not_called()\n\n\n@pytest.mark.parametrize(\n    \"packages, path_in_params\",\n    [(None, True), (None, False), ([\"pkg1\", \"pkg2\"], False), ([\"pkg1\", \"pkg2\"], True)],\n)\ndef test_install_project_callback(mocker, context_mock, micropy_obj, packages, path_in_params):\n    if context_mock.resilient_parsing:\n        assert main_app.install_local_callback(context_mock, packages) is None\n        return\n    context_mock.params = dict()\n    if path_in_params:\n        context_mock.params[\"path\"] = \"some_path\"\n    context_mock.params[\"dev\"] = False\n\n    if path_in_params:\n        result = main_app.install_project_callback(context_mock, packages)\n        assert result is None\n        return\n\n    if packages is None and not path_in_params:\n        with pytest.raises(typer.Exit):\n            main_app.install_project_callback(context_mock, packages)\n            micropy_obj.project.add_from_file.assert_called_once()\n        return\n\n    result = main_app.install_project_callback(context_mock, packages)\n    assert result == packages\n\n\n@pytest.mark.parametrize(\"dev_flag\", [True, False])\ndef test_main_install(mocker, micropy_obj, runner, dev_flag):\n    packages = [\"package1\", \"package2\"]\n    result = runner.invoke(\n        app,\n        [\"install\", *packages, \"--dev\"] if dev_flag else [\"install\", *packages],\n        obj=micropy_obj,\n    )\n    print(result.stdout)\n\n    assert result.exit_code == 0\n    assert \"Installing Packages\" in result.stdout\n\n    for pkg in packages:\n        micropy_obj.project.add_package.assert_any_call(pkg, dev=dev_flag)\n\n\ndef test_main_version(runner):\n    result = runner.invoke(app, [\"version\"])\n    assert result.exit_code == 0\n    assert \"Micropy Version:\" in result.stdout\n"
  },
  {
    "path": "tests/app/test_stubs.py",
    "content": "from pathlib import Path\n\nimport pytest\nfrom micropy.app import stubs as stubs_app\nfrom micropy.app.stubs import stubs_app as app\nfrom micropy.exceptions import StubError, StubNotFound\nfrom micropy.pyd import PyDevice\nfrom micropy.stubs import StubRepositoryPackage\nfrom micropy.stubs.source import StubSource\nfrom pytest_mock import MockerFixture\nfrom stubber.codemod.modify_list import ListChangeSet\nfrom tests.app.conftest import MicroPyScenario\n\n\n@pytest.mark.parametrize(\n    \"input,expected\",\n    [(None, None), ([\"mod-1\", \"mod-2\"], ListChangeSet.from_strings(add=[\"mod-1\", \"mod-2\"]))],\n)\ndef test_create_changeset(input, expected):\n    result = stubs_app.create_changeset(input)\n    if expected is None:\n        assert result is None\n    else:\n        assert result.add[0].children == expected.add[0].children\n        assert result.add[1].children == expected.add[1].children\n\n\n@pytest.fixture()\ndef pydevice_mock(mocker: MockerFixture):\n    def mock_copy_from(dev_path, tmp_dir, **kwargs):\n        stub_dir = Path(tmp_dir) / \"stubs\"\n        stub_dir.mkdir()\n\n    pyb_mock = mocker.MagicMock(PyDevice, autospec=True)\n    pyb_mock.return_value.copy_from = mock_copy_from\n    return pyb_mock\n\n\n@pytest.fixture()\ndef pyb_mock(request: pytest.FixtureRequest, mocker: MockerFixture):\n    device_mock = request.getfixturevalue(\"pydevice_mock\")\n    pyb = device_mock.return_value\n    mocker.patch(\"micropy.app.stubs.PyDevice\", return_value=pyb)\n    return pyb\n\n\n@pytest.fixture\ndef stubs_locator_mock(mocker: MockerFixture):\n    stubs_locator = mocker.MagicMock(StubSource, autospec=True)\n    mocker.patch(\"micropy.app.stubs.stubs_source.StubSource\", return_value=stubs_locator)\n    return stubs_locator\n\n\n@pytest.fixture()\ndef stub_search_data(mocker: MockerFixture):\n    stub1 = mocker.MagicMock(StubRepositoryPackage, autospec=True)\n    stub2 = mocker.MagicMock(StubRepositoryPackage, autospec=True)\n    stub3 = mocker.MagicMock(StubRepositoryPackage, autospec=True)\n    stub1.name = \"test1\"\n    stub1.version = \"1.0.0\"\n    stub2.name = \"test2\"\n    stub2.version = \"1.1.0\"\n    stub3.name = \"test3\"\n    stub3.version = \"0.9.0\"\n    return [\n        stub1,\n        stub2,\n        stub3,\n    ]\n\n\ndef test_stubs_create(mocker: MockerFixture, pyb_mock, micropy_obj, runner):\n    result = runner.invoke(app, [\"create\", \"/dev/port\"], obj=micropy_obj)\n    print(result.stdout)\n    pyb_mock.run_script.assert_called_once()\n    pyb_mock.disconnect.assert_called_once()\n\n\ndef test_stubs_create__connect_error(pydevice_mock, micropy_obj, runner):\n    pydevice_mock.side_effect = SystemExit()\n    result = runner.invoke(app, [\"create\", \"/dev/port\"], obj=micropy_obj)\n    assert result.return_value is None\n\n\ndef test_stubs_create__script_error(pyb_mock, micropy_obj, runner):\n    pyb_mock.run_script.side_effect = Exception(\"Script error\")\n    with pytest.raises(Exception, match=\"Script error\"):\n        result = runner.invoke(\n            app, [\"create\", \"/dev/port\"], obj=micropy_obj, catch_exceptions=False\n        )\n        assert result.return_value is None\n\n\n@pytest.mark.parametrize(\"force\", [True, False])\n@pytest.mark.parametrize(\"micropy_obj\", [MicroPyScenario(impl_add=False)], indirect=True)\ndef test_stubs_add_success(micropy_obj, runner, stubs_locator_mock, mock_repo, force):\n    stubs_locator_mock.ready.return_value.__enter__.return_value = \"test-stub\"\n    args = [\"add\", \"test-stub\"]\n    if force:\n        args.append(\"--force\")\n    result = runner.invoke(app, args, obj=micropy_obj, catch_exceptions=False)\n    print(result.stdout)\n    assert result.exit_code == 0\n    assert \"added!\" in result.stdout\n    stubs_locator_mock.ready.assert_called_once_with(\"test-stub\")\n    micropy_obj.stubs.add.assert_called_once_with(\"test-stub\", force=force)\n\n\n@pytest.mark.parametrize(\"micropy_obj\", [MicroPyScenario(impl_add=False)], indirect=True)\ndef test_stubs_add__not_found(micropy_obj, runner, stubs_locator_mock, mock_repo):\n    micropy_obj.stubs.add.side_effect = StubNotFound()\n    result = runner.invoke(app, [\"add\", \"nonexistent-stub\"], obj=micropy_obj)\n    assert result.exit_code == 1\n    assert \"could not be found\" in result.stdout\n    stubs_locator_mock.ready.assert_called_once_with(\"nonexistent-stub\")\n\n\n@pytest.mark.parametrize(\"micropy_obj\", [MicroPyScenario(impl_add=False)], indirect=True)\ndef test_stubs_add__invalid(micropy_obj, runner, stubs_locator_mock, mock_repo):\n    micropy_obj.stubs.add.side_effect = StubError()\n    result = runner.invoke(app, [\"add\", \"invalid-stub\"], obj=micropy_obj)\n    assert result.exit_code == 1\n    assert \"is not a valid stub!\" in result.stdout\n    stubs_locator_mock.ready.assert_called_once_with(\"invalid-stub\")\n\n\n@pytest.mark.parametrize(\n    \"micropy_obj\", [MicroPyScenario(), MicroPyScenario(project_exists=False)], indirect=True\n)\ndef test_stubs_list(micropy_obj, runner):\n    result = runner.invoke(app, [\"list\"], obj=micropy_obj)\n    assert result.exit_code == 0\n    assert \"Installed Stubs\" in result.stdout\n    print(result.stdout)\n    if not micropy_obj.project.exists:\n        micropy_obj.stubs.iter_by_firmware.assert_called_once()\n    else:\n        assert micropy_obj.stubs.iter_by_firmware.call_count == 2\n\n\n@pytest.mark.parametrize(\"outdated\", [True, False])\ndef test_stubs_search(stub_search_data, micropy_obj, runner, outdated):\n    micropy_obj.stubs._loaded = {\"test1\", \"test3\"}\n    micropy_obj.stubs._firmware = {\"test1\", \"test3\"}\n    micropy_obj.repo.search.return_value = stub_search_data\n\n    args = [\"search\", \"test\"]\n    if outdated:\n        args.append(\"--show-outdated\")\n    result = runner.invoke(app, args, obj=micropy_obj, catch_exceptions=False)\n\n    assert result.exit_code == 0\n    assert \"Results for test\" in result.stdout\n    assert \"test1\" in result.stdout\n    assert \"test2\" in result.stdout\n    assert \"test3\" in result.stdout\n    assert \"Installed\" in result.stdout\n\n\ndef test_stubs_search_no_results(mocker: MockerFixture, micropy_obj, runner):\n    micropy_obj.repo.search.return_value = []\n\n    result = runner.invoke(app, [\"search\", \"nonexistent\"], obj=micropy_obj)\n\n    assert result.exit_code == 0\n    assert \"No results found for: nonexistent\" in result.stdout\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\"\"\" Common Pytest Fixtures\"\"\"\n\nimport importlib\nimport shutil\nfrom pathlib import Path\nfrom pprint import PrettyPrinter\n\nimport micropy\nimport pytest\nimport questionary\nfrom boltons import iterutils\n\n# Mock values for Template VSCode ext checks\nmock_vscode_exts = [\n    \"mock.ext@0.0.0\",\n    # meets req\n    \"ms-python.python@2019.9.34474\",\n]\n\n\ndef pytest_collection_modifyitems(items):\n    items.reverse()\n\n\n@pytest.fixture(autouse=True)\ndef cleanup_data(mocker):\n    mocker.resetall()\n    try:\n        micropy.utils.ensure_valid_url.clear_cache()\n    except Exception:\n        importlib.reload(micropy)\n\n\n@pytest.fixture\ndef mock_prompt(monkeypatch):\n    def mock_prompt(*args, **kwargs):\n        class prompt_mock:\n            def __init__(self, *args, **kwargs):\n                return None\n\n            def ask(self):\n                return [\"stub\"]\n\n        return prompt_mock(*args, **kwargs)\n\n    monkeypatch.setattr(questionary, \"checkbox\", mock_prompt)\n\n\n@pytest.fixture\ndef mock_micropy_path(mocker, tmp_path):\n    path = tmp_path / \".micropy\"\n    stub_path = path / \"stubs\"\n    log_path = path / \"micropy.log\"\n    mocker.patch(\"micropy.data.FILES\", path)\n    mocker.patch(\"micropy.data.STUB_DIR\", stub_path)\n    mocker.patch(\"micropy.data.LOG_FILE\", log_path)\n    return path\n\n\n@pytest.fixture\ndef mock_micropy(mock_micropy_path):\n    config = micropy.main.MicroPyOptions(root_dir=mock_micropy_path)\n    mp = micropy.main.MicroPy(options=config)\n    return mp\n\n\n@pytest.fixture\ndef mock_cwd(request, tmp_path, mocker):\n    print(request)\n    import pathlib\n\n    mocker.patch(\"pathlib.Path.cwd\")\n    pathlib.Path.cwd.return_value = tmp_path\n    yield (tmp_path)\n\n\n@pytest.fixture(scope=\"session\")\ndef test_urls():\n    def test_headers(type):\n        return {\"content-type\": type}\n\n    return {\n        \"valid\": \"http://www.google.com\",\n        \"valid_https\": \"https://www.google.com\",\n        \"invalid\": \"/foobar/bar/foo\",\n        \"invalid_file\": \"file:///foobar/bar/foo\",\n        \"bad_resp\": \"http://www.google.com/XYZ/ABC/BADRESP\",\n        \"download\": \"https://www.somewebsite.com/archive_test_stub.tar.gz\",\n        \"headers\": {\n            \"can_download\": test_headers(\"application/gzip\"),\n            \"not_download\": test_headers(\"text/plain\"),\n        },\n    }\n\n\n@pytest.fixture\ndef get_stub_paths(shared_datadir, tmp_path):\n    def _get_stub_paths(count=1, valid=True, firm=False, dest=tmp_path):\n        _stubs = [\"fware\"] if firm else [\"esp8266\", \"esp32\"]\n        stubs = iter(_stubs)\n        _count = 0\n        while _count < count:\n            s = next(stubs)\n            path = (\n                (shared_datadir / f\"{s}_test_stub\")\n                if valid\n                else (shared_datadir / f\"{s}_invalid_stub\")\n            )\n            if path.exists():\n                dest = dest / path.name\n                if not dest.exists():\n                    shutil.copytree(path, (dest / path.name))\n                yield dest\n                _count += 1\n\n    return _get_stub_paths\n\n\n@pytest.fixture\ndef mock_mp_stubs(mock_micropy, mocker, shared_datadir):\n    mock_micropy.stubs.add(shared_datadir / \"fware_test_stub\")\n    mock_micropy.stubs.add(shared_datadir / \"esp8266_test_stub\")\n    mock_micropy.stubs.add(shared_datadir / \"esp32_test_stub\")\n    return mock_micropy\n\n\n@pytest.fixture\ndef get_stubs(get_stub_paths, mocker, tmp_path):\n    def _get_stubs(path=tmp_path, **kwargs):\n        def stubbify(m, path, firm=None):\n            m.path = path\n            m.frozen = path / \"frozen\"\n            m.stubs = path / \"stubs\"\n            m.name = m.path.name\n            m.stub_version = \"0.0.0\"\n            m.firmware = firm\n            return m\n\n        paths = get_stub_paths(dest=path, **kwargs)\n        firm = next(get_stub_paths(firm=True, dest=path))\n        firm_mock = stubbify(mocker.MagicMock(), firm)\n        for p in paths:\n            yield stubbify(mocker.MagicMock(), p, firm=firm_mock)\n\n    return _get_stubs\n\n\n@pytest.fixture\ndef micropy_stubs(mocker, get_stubs):\n    def _micropy_stubs(count=3):\n        def _mock_resolve_subresource(stubs, data_path):\n            return get_stubs(path=data_path)\n\n        mock_mp = mocker.patch.object(micropy, \"MicroPy\").return_value\n        stubs = list(get_stubs())\n        mock_mp.stubs.__iter__.return_value = stubs\n        mock_mp.stubs.resolve_subresource = _mock_resolve_subresource\n        mock_mp.stubs.add.return_value = stubs[0]\n        return mock_mp\n\n    return _micropy_stubs\n\n\n@pytest.yield_fixture\ndef test_archive(shared_datadir):\n    archive = shared_datadir / \"archive_test_stub.tar.gz\"\n    file_obj = archive.open(\"rb\")\n    file_bytes = file_obj.read()\n    yield file_bytes\n    file_obj.close()\n\n\nmicropy_source = micropy.stubs.repository_info.RepositoryInfo(\n    name=\"BradenM/micropy-stubs\",\n    display_name=\"micropy-stubs\",\n    source=\"https://my-mocked-source.com/bradenm\",\n)\n\nmicropython_source = micropy.stubs.repository_info.RepositoryInfo(\n    name=\"Josverl/micropython-stubs\",\n    display_name=\"micropython-stubs\",\n    # source=\"https://raw.githubusercontent.com/Josverl/micropython-stubs/main/publish/package_data.jsondb\",\n    source=\"https://my-mocked-source.com/josverl\",\n)\n\n\n@pytest.fixture\ndef mock_manifests(mocker, requests_mock):\n    micropy_manifest = {\n        \"name\": \"Micropy Stubs\",\n        \"location\": \"https://codeload.github.com/BradenM/micropy-stubs\",\n        \"source\": \"https://raw.githubusercontent.com/bradenm/micropy-stubs/source.json\",\n        \"path\": \"legacy.tar.gz/pkg/\",\n        \"packages\": [\n            {\n                \"name\": \"micropython\",\n                \"type\": \"firmware\",\n                \"sha256sum\": \"7ff2cce0237268cd52164b77b6c2df6be6249a67ee285edc122960af869b8ed2\",\n            },\n            {\"name\": \"esp8266-micropython-1.15.0\", \"type\": \"device\", \"sha256sum\": \"abc123\"},\n        ],\n    }\n    micropython_manifest = {\n        \"version\": 2,\n        \"keys\": [\n            \"description\",\n            \"hash\",\n            \"mpy_version\",\n            \"name\",\n            \"path\",\n            \"pkg_version\",\n            \"publish\",\n            \"stub_hash\",\n            \"stub_sources\",\n        ],\n        \"data\": {\n            \"160521968180811532\": {\n                \"name\": \"micropython-esp32-stubs\",\n                \"mpy_version\": \"1.18\",\n                \"publish\": True,\n                \"pkg_version\": \"1.18.post1\",\n                \"path\": \"publish/micropython-v1_18-esp32-stubs\",\n                \"stub_sources\": [\n                    [\"Firmware stubs\", \"stubs/micropython-v1_18-esp32\"],\n                    [\"Frozen stubs\", \"stubs/micropython-v1_18-frozen/esp32/GENERIC\"],\n                    [\"Core Stubs\", \"stubs/cpython_core-pycopy\"],\n                ],\n                \"description\": \"MicroPython stubs\",\n                \"hash\": \"712ebd85140b078ce6d9d3cbb9d7ffc18cf10aef\",\n                \"stub_hash\": \"\",\n            }\n        },\n    }\n\n    requests_mock.get(\n        micropy_source.source,\n        json=micropy_manifest,\n    )\n    requests_mock.get(\n        micropython_source.source,\n        json=micropython_manifest,\n    )\n\n\n@pytest.fixture\ndef mock_checks(mocker):\n    \"\"\"Mock VSCode Template Checks\"\"\"\n    m_run = mocker.patch.object(micropy.project.checks.subproc, \"run\").return_value\n    type(m_run).stdout = mocker.PropertyMock(return_value=\"\\n\".join(mock_vscode_exts))\n    return m_run\n\n\n@pytest.fixture\ndef mock_pkg(mocker, tmp_path):\n    \"\"\"return mock package\"\"\"\n    from micropy import packages\n\n    tmp_pkg = tmp_path / \"tmp_pkg\"\n    tmp_pkg.mkdir()\n    (tmp_pkg / \"module.py\").touch()\n    (tmp_pkg / \"file.py\").touch()\n    mocker.patch.object(packages.source_package.utils, \"ensure_valid_url\")\n    mock_tarbytes = mocker.patch.object(packages.source_package.utils, \"extract_tarbytes\")\n    mock_meta = mocker.patch.object(packages.source_package.utils, \"get_package_meta\")\n    mocker.patch.object(packages.source_package.utils, \"get_url_filename\")\n    mocker.patch.object(packages.source_package.utils, \"stream_download\")\n    mock_tarbytes.return_value = tmp_pkg\n    mock_meta.return_value = {\"url\": \"http://realurl.com\"}\n    return tmp_pkg\n\n\n# Pytest Incremental Marker\ndef pytest_runtest_makereport(item, call):\n    if \"incremental\" in item.keywords:\n        if call.excinfo is not None:\n            parent = item.parent\n            parent._previousfailed = item\n\n\ndef pytest_runtest_setup(item):\n    if \"incremental\" in item.keywords:\n        previousfailed = getattr(item.parent, \"_previousfailed\", None)\n        if previousfailed is not None:\n            pytest.xfail(f\"previous test failed ({previousfailed.name})\")\n\n\nclass AssertUtils:\n    pp = PrettyPrinter(indent=4, width=20).pprint\n\n    def dict_match_mocks(self, d1):\n        from unittest.mock import Mock\n\n        # Mocks\n        remapped = iterutils.remap(\n            d1, lambda p, k, v: (k, \"MOCKED_VALUE\") if isinstance(v, Mock) else True\n        )\n        return remapped\n\n    def dict_equal(self, d1, d2):\n        match_d1 = sorted(self.dict_match_mocks(d1).items())\n        match_d2 = sorted(self.dict_match_mocks(d1).items())\n        print(\"== IS DICT EQUAL ==\")\n        self.pp(match_d1)\n        print(\"\\n----------\\n\")\n        self.pp(match_d2)\n        print(\"==============\")\n        return match_d1 == match_d2\n\n    def list_equal(self, l1, l2):\n        list_one = sorted(l1)\n        list_two = sorted(l2)\n        return list_one == list_two\n\n    def load_json(self, path):\n        import json\n\n        return json.loads(path.read_text())\n\n    def json_equal_dict(self, path, d2):\n        data = self.load_json(path)\n        return self.dict_equal(data, d2)\n\n    def str_path(self, path, absolute=False):\n        \"\"\"x-platform path strings helper\"\"\"\n        path = Path(path)\n        if absolute:\n            path = path.absolute()\n        return str(path)\n\n    def get_rand_str(self, length=10):\n        import random\n        import string\n\n        letters = string.ascii_lowercase\n        return \"\".join(random.choice(letters) for i in range(length))\n\n\n@pytest.fixture\ndef utils():\n    return AssertUtils()\n"
  },
  {
    "path": "tests/data/esp32_test_stub/frozen/ntptime.py",
    "content": "try:\n    import usocket as socket\nexcept:\n    import socket\ntry:\n    import ustruct as struct\nexcept:\n    import struct\n\n# (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60\nNTP_DELTA = 3155673600\n\nhost = \"pool.ntp.org\"\n\n\ndef time():\n    NTP_QUERY = bytearray(48)\n    NTP_QUERY[0] = 0x1B\n    addr = socket.getaddrinfo(host, 123)[0][-1]\n    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n    s.settimeout(1)\n    res = s.sendto(NTP_QUERY, addr)\n    msg = s.recv(48)\n    s.close()\n    val = struct.unpack(\"!I\", msg[40:44])[0]\n    return val - NTP_DELTA\n\n\n# There's currently no timezone support in MicroPython, so\n# utime.localtime() will return UTC time (as if it was .gmtime())\ndef settime():\n    t = time()\n    import machine\n    import utime\n\n    tm = utime.localtime(t)\n    tm = tm[0:3] + (0,) + tm[3:6] + (0,)\n    machine.RTC().datetime(tm)\n    print(utime.localtime())\n"
  },
  {
    "path": "tests/data/esp32_test_stub/frozen/ntptime.pyi",
    "content": "# make_stub_files: Fri 21 Jun 2019 at 00:44:00\n\nfrom typing import Any, Dict, Optional, Sequence, Tuple, Union\n\nNode = Any\n\ndef time() -> Any: ...\n\n#   0: return val-NTP_DELTA\n# ? 0: return val-NTP_DELTA\ndef settime() -> None: ...\n"
  },
  {
    "path": "tests/data/esp32_test_stub/info.json",
    "content": "{\n    \"firmware\": {\n        \"machine\": \"ESP32 module with ESP32\",\n        \"firmware\": \"esp32 1.11.0\",\n        \"nodename\": \"esp8266\",\n        \"version\": \"1.11.0\",\n        \"release\": \"1.11.0\",\n        \"sysname\": \"esp32\",\n        \"name\": \"micropython\"\n    },\n    \"stubber\": { \"version\": \"1.2.0\" },\n    \"modules\": [\n        {\n            \"file\": \"/stubs/esp32_1_11_0/umqtt/robust.py\",\n            \"module\": \"umqtt.robust\"\n        },\n        {\n            \"file\": \"/stubs/esp32_1_11_0/umqtt/simple.py\",\n            \"module\": \"umqtt.simple\"\n        }\n    ]\n}\n"
  },
  {
    "path": "tests/data/esp32_test_stub/stubs/machine.py",
    "content": "\"\"\"\nModule: 'machine' on esp8266 v1.9.4\n\"\"\"\n# MCU: (sysname='esp8266', nodename='esp8266', release='2.2.0-dev(9422289)', version='v1.9.4-8-ga9a3caad0 on 2018-05-11', machine='ESP module with ESP8266')\n# Stubber: 1.1.2\n\n\nclass ADC:\n    \"\"\"\"\"\"\n\n    def read():\n        pass\n\n\nDEEPSLEEP = 4\nDEEPSLEEP_RESET = 5\nHARD_RESET = 6\n\n\nclass I2C:\n    \"\"\"\"\"\"\n"
  },
  {
    "path": "tests/data/esp32_test_stub/stubs/modules.json",
    "content": "[\n  {\n    \"nodename\": \"esp8266\",\n    \"release\": \"2.2.0-dev(9422289)\",\n    \"version\": \"v1.9.4-8-ga9a3caad0 on 2018-05-11\",\n    \"machine\": \"ESP module with ESP8266\",\n    \"sysname\": \"esp8266\"\n  },\n  { \"stubber\": \"1.1.2\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/uasyncio/core.py\",\n    \"module\": \"uasyncio.core\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/umqtt/robust.py\", \"module\": \"umqtt.robust\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/umqtt/simple.py\", \"module\": \"umqtt.simple\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/urllib/urequest.py\",\n    \"module\": \"urllib.urequest\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/upip.py\", \"module\": \"upip\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/_boot.py\", \"module\": \"_boot\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/_onewire.py\", \"module\": \"_onewire\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/apa102.py\", \"module\": \"apa102\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/array.py\", \"module\": \"array\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/btree.py\", \"module\": \"btree\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/dht.py\", \"module\": \"dht\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ds18x20.py\", \"module\": \"ds18x20\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/errno.py\", \"module\": \"errno\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/esp.py\", \"module\": \"esp\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/example_pub_button.py\",\n    \"module\": \"example_pub_button\"\n  },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/example_sub_led.py\",\n    \"module\": \"example_sub_led\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/flashbdev.py\", \"module\": \"flashbdev\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/framebuf.py\", \"module\": \"framebuf\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/gc.py\", \"module\": \"gc\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/inisetup.py\", \"module\": \"inisetup\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/json.py\", \"module\": \"json\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/math.py\", \"module\": \"math\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/micropython.py\", \"module\": \"micropython\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/neopixel.py\", \"module\": \"neopixel\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/network.py\", \"module\": \"network\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ntptime.py\", \"module\": \"ntptime\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/onewire.py\", \"module\": \"onewire\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/select.py\", \"module\": \"select\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/sys.py\", \"module\": \"sys\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/time.py\", \"module\": \"time\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ubinascii.py\", \"module\": \"ubinascii\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uhashlib.py\", \"module\": \"uhashlib\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uheapq.py\", \"module\": \"uheapq\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ujson.py\", \"module\": \"ujson\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/urandom.py\", \"module\": \"urandom\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ure.py\", \"module\": \"ure\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uselect.py\", \"module\": \"uselect\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ussl.py\", \"module\": \"ussl\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ustruct.py\", \"module\": \"ustruct\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/utime.py\", \"module\": \"utime\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/utimeq.py\", \"module\": \"utimeq\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uzlib.py\", \"module\": \"uzlib\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/websocket_helper.py\",\n    \"module\": \"websocket_helper\"\n  }\n]\n"
  },
  {
    "path": "tests/data/esp8266_invalid_stub/info.json",
    "content": "[\n    {\n        \"nodename\": \"esp32\",\n        \"release\": \"1.10.0\"\n    },\n    {\n        \"pathtofile\": \"/foobar/foo/bar.py\",\n        \"something\": \"bar\"\n    }\n]\n"
  },
  {
    "path": "tests/data/esp8266_test_stub/frozen/ntptime.py",
    "content": "try:\n    import usocket as socket\nexcept:\n    import socket\ntry:\n    import ustruct as struct\nexcept:\n    import struct\n\n# (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60\nNTP_DELTA = 3155673600\n\nhost = \"pool.ntp.org\"\n\n\ndef time():\n    NTP_QUERY = bytearray(48)\n    NTP_QUERY[0] = 0x1B\n    addr = socket.getaddrinfo(host, 123)[0][-1]\n    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n    s.settimeout(1)\n    res = s.sendto(NTP_QUERY, addr)\n    msg = s.recv(48)\n    s.close()\n    val = struct.unpack(\"!I\", msg[40:44])[0]\n    return val - NTP_DELTA\n\n\n# There's currently no timezone support in MicroPython, so\n# utime.localtime() will return UTC time (as if it was .gmtime())\ndef settime():\n    t = time()\n    import machine\n    import utime\n\n    tm = utime.localtime(t)\n    tm = tm[0:3] + (0,) + tm[3:6] + (0,)\n    machine.RTC().datetime(tm)\n    print(utime.localtime())\n"
  },
  {
    "path": "tests/data/esp8266_test_stub/frozen/ntptime.pyi",
    "content": "# make_stub_files: Fri 21 Jun 2019 at 00:44:00\n\nfrom typing import Any, Dict, Optional, Sequence, Tuple, Union\n\nNode = Any\n\ndef time() -> Any: ...\n\n#   0: return val-NTP_DELTA\n# ? 0: return val-NTP_DELTA\ndef settime() -> None: ...\n"
  },
  {
    "path": "tests/data/esp8266_test_stub/info.json",
    "content": "{\n    \"firmware\": {\n        \"machine\": \"ESP module with ESP8266\",\n        \"firmware\": \"esp8266 v1.9.4\",\n        \"nodename\": \"esp8266\",\n        \"version\": \"1.9.4\",\n        \"release\": \"2.2.0-dev(9422289)\",\n        \"sysname\": \"esp8266\",\n        \"name\": \"micropython\"\n    },\n    \"stubber\": { \"version\": \"1.2.0\" },\n    \"modules\": [\n        {\n            \"file\": \"/stubs/esp8266_1_9_4/umqtt/robust.py\",\n            \"module\": \"umqtt.robust\"\n        },\n        {\n            \"file\": \"/stubs/esp8266_1_9_4/umqtt/simple.py\",\n            \"module\": \"umqtt.simple\"\n        }\n    ]\n}\n"
  },
  {
    "path": "tests/data/esp8266_test_stub/stubs/machine.py",
    "content": "\"\"\"\nModule: 'machine' on esp8266 v1.9.4\n\"\"\"\n# MCU: (sysname='esp8266', nodename='esp8266', release='2.2.0-dev(9422289)', version='v1.9.4-8-ga9a3caad0 on 2018-05-11', machine='ESP module with ESP8266')\n# Stubber: 1.1.2\n\n\nclass ADC:\n    \"\"\"\"\"\"\n\n    def read():\n        pass\n\n\nDEEPSLEEP = 4\nDEEPSLEEP_RESET = 5\nHARD_RESET = 6\n\n\nclass I2C:\n    \"\"\"\"\"\"\n"
  },
  {
    "path": "tests/data/esp8266_test_stub/stubs/modules.json",
    "content": "[\n  {\n    \"nodename\": \"esp8266\",\n    \"release\": \"2.2.0-dev(9422289)\",\n    \"version\": \"v1.9.4-8-ga9a3caad0 on 2018-05-11\",\n    \"machine\": \"ESP module with ESP8266\",\n    \"sysname\": \"esp8266\"\n  },\n  { \"stubber\": \"1.1.2\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/uasyncio/core.py\",\n    \"module\": \"uasyncio.core\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/umqtt/robust.py\", \"module\": \"umqtt.robust\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/umqtt/simple.py\", \"module\": \"umqtt.simple\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/urllib/urequest.py\",\n    \"module\": \"urllib.urequest\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/upip.py\", \"module\": \"upip\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/_boot.py\", \"module\": \"_boot\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/_onewire.py\", \"module\": \"_onewire\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/apa102.py\", \"module\": \"apa102\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/array.py\", \"module\": \"array\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/btree.py\", \"module\": \"btree\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/dht.py\", \"module\": \"dht\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ds18x20.py\", \"module\": \"ds18x20\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/errno.py\", \"module\": \"errno\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/esp.py\", \"module\": \"esp\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/example_pub_button.py\",\n    \"module\": \"example_pub_button\"\n  },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/example_sub_led.py\",\n    \"module\": \"example_sub_led\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/flashbdev.py\", \"module\": \"flashbdev\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/framebuf.py\", \"module\": \"framebuf\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/gc.py\", \"module\": \"gc\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/inisetup.py\", \"module\": \"inisetup\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/json.py\", \"module\": \"json\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/math.py\", \"module\": \"math\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/micropython.py\", \"module\": \"micropython\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/neopixel.py\", \"module\": \"neopixel\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/network.py\", \"module\": \"network\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ntptime.py\", \"module\": \"ntptime\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/onewire.py\", \"module\": \"onewire\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/select.py\", \"module\": \"select\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/sys.py\", \"module\": \"sys\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/time.py\", \"module\": \"time\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ubinascii.py\", \"module\": \"ubinascii\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uhashlib.py\", \"module\": \"uhashlib\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uheapq.py\", \"module\": \"uheapq\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ujson.py\", \"module\": \"ujson\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/urandom.py\", \"module\": \"urandom\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ure.py\", \"module\": \"ure\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uselect.py\", \"module\": \"uselect\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ussl.py\", \"module\": \"ussl\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ustruct.py\", \"module\": \"ustruct\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/utime.py\", \"module\": \"utime\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/utimeq.py\", \"module\": \"utimeq\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uzlib.py\", \"module\": \"uzlib\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/websocket_helper.py\",\n    \"module\": \"websocket_helper\"\n  }\n]\n"
  },
  {
    "path": "tests/data/fware_test_stub/frozen/utarfile.py",
    "content": "import uctypes\n\n# http://www.gnu.org/software/tar/manual/html_node/Standard.html\nTAR_HEADER = {\n    \"name\": (uctypes.ARRAY | 0, uctypes.UINT8 | 100),\n    \"size\": (uctypes.ARRAY | 124, uctypes.UINT8 | 12),\n}\n\nDIRTYPE = \"dir\"\nREGTYPE = \"file\"\n\n\ndef roundup(val, align):\n    return (val + align - 1) & ~(align - 1)\n\n\nclass FileSection:\n    def __init__(self, f, content_len, aligned_len):\n        self.f = f\n        self.content_len = content_len\n        self.align = aligned_len - content_len\n\n    def read(self, sz=65536):\n        if self.content_len == 0:\n            return b\"\"\n        if sz > self.content_len:\n            sz = self.content_len\n        data = self.f.read(sz)\n        sz = len(data)\n        self.content_len -= sz\n        return data\n\n    def readinto(self, buf):\n        if self.content_len == 0:\n            return 0\n        if len(buf) > self.content_len:\n            buf = memoryview(buf)[: self.content_len]\n        sz = self.f.readinto(buf)\n        self.content_len -= sz\n        return sz\n\n    def skip(self):\n        self.f.read(self.content_len + self.align)\n\n\nclass TarInfo:\n    def __str__(self):\n        return \"TarInfo(%r, %s, %d)\" % (self.name, self.type, self.size)\n\n\nclass TarFile:\n    def __init__(self, name=None, fileobj=None):\n        if fileobj:\n            self.f = fileobj\n        else:\n            self.f = open(name, \"rb\")\n        self.subf = None\n\n    def next(self):\n        if self.subf:\n            self.subf.skip()\n        buf = self.f.read(512)\n        if not buf:\n            return None\n\n        h = uctypes.struct(uctypes.addressof(buf), TAR_HEADER, uctypes.LITTLE_ENDIAN)\n\n        # Empty block means end of archive\n        if h.name[0] == 0:\n            return None\n\n        d = TarInfo()\n        d.name = str(h.name, \"utf-8\").rstrip()\n        d.size = int(bytes(h.size).rstrip(), 8)\n        d.type = [REGTYPE, DIRTYPE][d.name[-1] == \"/\"]\n        self.subf = d.subf = FileSection(self.f, d.size, roundup(d.size, 512))\n        return d\n\n    def __iter__(self):\n        return self\n\n    def __next__(self):\n        v = self.next()\n        if v is None:\n            raise StopIteration\n        return v\n\n    def extractfile(self, tarinfo):\n        return tarinfo.subf\n"
  },
  {
    "path": "tests/data/fware_test_stub/frozen/utarfile.pyi",
    "content": "# make_stub_files: Thu 20 Jun 2019 at 23:08:04\n\nfrom typing import Any, Dict, Optional, Sequence, Tuple, Union\n\nNode = Any\n\ndef roundup(val: Any, align: Any) -> Any: ...\n\n#   0: return val+align-1&~align-1\n# ? 0: return val+align-number&align-number\nclass FileSection:\n    def __init__(self, f: Any, content_len: smallint, aligned_len: smallint) -> None: ...\n    def read(self, sz: Any = 65536) -> Union[Any, bytes]: ...\n    #   0: return b''\n    #   0: return bytes\n    #   1: return data\n    # ? 1: return data\n    def readinto(self, buf: Any) -> Union[Any, number]: ...\n    #   0: return 0\n    #   0: return number\n    #   1: return sz\n    # ? 1: return sz\n    def skip(self) -> None: ...\n\nclass TarInfo:\n    def __str__(self) -> str: ...\n\nclass TarFile:\n    def __init__(self, name: str = None, fileobj: Any = None) -> None: ...\n    def next(self) -> Optional[Any]: ...\n    #   0: return None\n    #   0: return None\n    #   1: return None\n    #   1: return None\n    #   2: return d\n    # ? 2: return d\n    def __iter__(self) -> Any: ...\n    #   0: return self\n    # ? 0: return self\n    def __next__(self) -> Any: ...\n    #   0: return v\n    # ? 0: return v\n    def extractfile(self, tarinfo: Any) -> Any: ...\n    #   0: return tarinfo.subf\n    # ? 0: return tarinfo.subf\n"
  },
  {
    "path": "tests/data/fware_test_stub/frozen/utokenize.py",
    "content": "# (c) 2019 Paul Sokolovsky, MIT license\nfrom token import *\n\nfrom ucollections import namedtuple\n\nNL = 55\nENCODING = 56\ntok_name[NL] = \"NL\"\ntok_name[ENCODING] = \"ENCODING\"\n\n\nclass TokenInfo(namedtuple(\"TokenInfo\", (\"type\", \"string\", \"start\", \"end\", \"line\"))):\n    def __str__(self):\n        return \"TokenInfo(type=%d (%s), string=%r, line=%r)\" % (\n            self.type,\n            tok_name[self.type],\n            self.string,\n            self.line,\n        )\n\n\ndef get_indent(l):\n    for i in range(len(l)):\n        if l[i] != \" \":\n            return i, l[i:]\n\n\ndef tokenize(readline):\n    indent = 0\n\n    yield TokenInfo(ENCODING, \"utf-8\", 0, 0, \"\")\n\n    while True:\n        l = readline()\n        org_l = l\n        if not l:\n            break\n        i, l = get_indent(l)\n\n        if l == \"\\n\":\n            yield TokenInfo(NL, l, 0, 0, org_l)\n            continue\n\n        if i > indent:\n            yield TokenInfo(INDENT, \" \" * (i - indent), 0, 0, org_l)\n        elif i < indent:\n            yield TokenInfo(DEDENT, \"\", 0, 0, org_l)\n        indent = i\n\n        while l:\n            if l[0].isdigit():\n                t = \"\"\n                while l and (l[0].isdigit() or l[0] == \".\"):\n                    t += l[0]\n                    l = l[1:]\n                yield TokenInfo(NUMBER, t, 0, 0, org_l)\n            elif l[0].isalpha():\n                name = \"\"\n                while l and (l[0].isalpha() or l[0].isdigit()):\n                    name += l[0]\n                    l = l[1:]\n                yield TokenInfo(NAME, name, 0, 0, org_l)\n            elif l[0] == \"\\n\":\n                yield TokenInfo(NEWLINE, \"\\n\", 0, 0, org_l)\n                break\n            elif l[0].isspace():\n                l = l[1:]\n            else:\n                yield TokenInfo(OP, l[0], 0, 0, org_l)\n                l = l[1:]\n\n    yield TokenInfo(ENDMARKER, \"\", 0, 0, \"\")\n"
  },
  {
    "path": "tests/data/fware_test_stub/frozen/utokenize.pyi",
    "content": "# make_stub_files: Thu 20 Jun 2019 at 23:08:04\n\nfrom typing import Any, Dict, Optional, Sequence, Tuple, Union\n\nNode = Any\n\nclass TokenInfo(namedtuple(str, Tuple[str, str, str, str, str])):\n    def __str__(self) -> str: ...\n\ndef get_indent(l: Any) -> Tuple[int, Any]: ...\ndef tokenize(readline: Any) -> None: ...\n"
  },
  {
    "path": "tests/data/fware_test_stub/info.json",
    "content": "{\n    \"scope\": \"firmware\",\n    \"name\": \"MicroPython Official\",\n    \"repo\": \"micropython/micropython\",\n    \"module_path\": \"ports/{}/modules\",\n    \"firmware\": \"micropython\",\n    \"excluded_modules\": [\n        \"_boot.py\",\n        \"inisetup.py\",\n        \"upip.py\",\n        \"upip_utarfile.py\"\n    ],\n    \"modules\": [\"utarfile\", \"utokenize\"],\n    \"devices\": [\"esp8266\", \"esp32\"],\n    \"path\": \"packages/micropython-official/info.json\",\n    \"versions\": [\n        {\n            \"version\": \"1.11.0\",\n            \"git_tag\": \"v1.11\",\n            \"sha\": \"6f75c4f3cd393131579db70cdf0b35d1fe5b95ab\",\n            \"latest\": true,\n            \"devices\": [\"esp8266\", \"esp32\"]\n        },\n        {\n            \"version\": \"1.10.0\",\n            \"git_tag\": \"v1.10\",\n            \"sha\": \"3e25d611ef3185b68558a20057d50b0d18dc67a0\",\n            \"latest\": false,\n            \"devices\": [\"esp8266\", \"esp32\"]\n        },\n        {\n            \"version\": \"1.9.4\",\n            \"git_tag\": \"v1.9.4\",\n            \"sha\": \"421b84af9968e582f324899934f52b3df60381ee\",\n            \"latest\": false,\n            \"devices\": [\"esp8266\", \"esp32\"]\n        },\n        {\n            \"version\": \"1.9.3\",\n            \"git_tag\": \"v1.9.3\",\n            \"sha\": \"fe45d78b1edd6d2202c3544797885cb0b12d4f03\",\n            \"latest\": false,\n            \"devices\": [\"esp8266\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "tests/data/project_test/.pylintrc",
    "content": "[MASTER]\n# Loaded Stubs:  esp32-micropython-1.11.0  esp8266-micropython-1.11.0\ninit-hook='import sys;sys.path.insert(1, \".micropy/micropython/frozen\");sys.path.insert(1, \".micropy/micropython/frozen\");sys.path.insert(1, \".micropy/esp32-micropython-1.11.0/frozen\");sys.path.insert(1, \".micropy/esp8266-micropython-1.11.0/frozen\");sys.path.insert(1, \".micropy/esp32-micropython-1.11.0/stubs\");sys.path.insert(1, \".micropy/esp8266-micropython-1.11.0/stubs\");sys.path.insert(1,\"./lib\")'\n"
  },
  {
    "path": "tests/data/project_test/.vscode/settings.json",
    "content": "{\n    \"python.linting.enabled\": true,\n\n    // Loaded Stubs:  esp32-micropython-1.11.0  esp8266-micropython-1.11.0\n    \"python.autoComplete.extraPaths\": [\n        \"${workspaceRoot}/.micropy/micropython/frozen\",\n        \"${workspaceRoot}/.micropy/micropython/frozen\",\n        \"${workspaceRoot}/.micropy/esp32-micropython-1.11.0/frozen\",\n        \"${workspaceRoot}/.micropy/esp8266-micropython-1.11.0/frozen\",\n        \"${workspaceRoot}/.micropy/esp32-micropython-1.11.0/stubs\",\n        \"${workspaceRoot}/.micropy/esp8266-micropython-1.11.0/stubs\"\n    ],\n    \"python.autoComplete.typeshedPaths\": [\n        \"${workspaceRoot}/.micropy/micropython/frozen\",\n        \"${workspaceRoot}/.micropy/micropython/frozen\",\n        \"${workspaceRoot}/.micropy/esp32-micropython-1.11.0/frozen\",\n        \"${workspaceRoot}/.micropy/esp8266-micropython-1.11.0/frozen\",\n        \"${workspaceRoot}/.micropy/esp32-micropython-1.11.0/stubs\",\n        \"${workspaceRoot}/.micropy/esp8266-micropython-1.11.0/stubs\"\n    ],\n    \"python.analysis.typeshedPaths\": [\n        \"${workspaceRoot}/.micropy/micropython/frozen\",\n        \"${workspaceRoot}/.micropy/micropython/frozen\",\n        \"${workspaceRoot}/.micropy/esp32-micropython-1.11.0/frozen\",\n        \"${workspaceRoot}/.micropy/esp8266-micropython-1.11.0/frozen\",\n        \"${workspaceRoot}/.micropy/esp32-micropython-1.11.0/stubs\",\n        \"${workspaceRoot}/.micropy/esp8266-micropython-1.11.0/stubs\"\n    ],\n    \"python.analysis.extraPaths\": [\n        \"${workspaceRoot}/.micropy/micropython/frozen\",\n        \"${workspaceRoot}/.micropy/micropython/frozen\",\n        \"${workspaceRoot}/.micropy/esp32-micropython-1.11.0/frozen\",\n        \"${workspaceRoot}/.micropy/esp8266-micropython-1.11.0/frozen\",\n        \"${workspaceRoot}/.micropy/esp32-micropython-1.11.0/stubs\",\n        \"${workspaceRoot}/.micropy/esp8266-micropython-1.11.0/stubs\"\n    ],\n\n    \"python.linting.pylintEnabled\": true\n}\n"
  },
  {
    "path": "tests/data/project_test/micropy.json",
    "content": "{\n    \"name\": \"NewProject\",\n    \"stubs\": {\n        \"esp32-micropython-1.11.0\": \"1.2.0\",\n        \"esp8266-micropython-1.11.0\": \"1.2.0\",\n        \"custom-stub\": \"../esp32_test_stub\"\n    },\n    \"config\": {\n        \"vscode\": true\n    },\n    \"packages\": {\n        \"some_pkg\": \"*\"\n    },\n    \"dev-packages\": {\n        \"dev_pkg\": \"*\"\n    }\n}\n"
  },
  {
    "path": "tests/data/stubber_test_stub/micropython.py",
    "content": "\"\"\"\nModule: 'micropython' on esp32 1.11.0\n\"\"\"\n\n\n# MCU: (sysname='esp32', nodename='esp32', release='1.11.0', version='v1.11-132-gc24d81119 on 2019-07-08', machine='ESP32 module with ESP32')\n# Stubber: 1.2.0\ndef alloc_emergency_exception_buf():\n    pass\n\n\ndef const():\n    pass\n\n\ndef heap_lock():\n    pass\n\n\ndef heap_unlock():\n    pass\n\n\ndef kbd_intr():\n    pass\n\n\ndef mem_info():\n    pass\n\n\ndef opt_level():\n    pass\n\n\ndef qstr_info():\n    pass\n\n\ndef schedule():\n    pass\n\n\ndef stack_use():\n    pass\n"
  },
  {
    "path": "tests/data/stubber_test_stub/modules.json",
    "content": "{\n    \"firmware\": {\n        \"machine\": \"ESP32 module with ESP32\",\n        \"firmware\": \"esp32 1.11.0\",\n        \"nodename\": \"esp32\",\n        \"version\": \"1.11.0\",\n        \"release\": \"1.11.0\",\n        \"sysname\": \"esp32\"\n    },\n    \"stubber\": { \"version\": \"1.2.0\" },\n    \"modules\": [\n        {\n            \"file\": \"/stubs/esp32_1_11_0/micropython.py\",\n            \"module\": \"micropython\"\n        }\n    ]\n}\n"
  },
  {
    "path": "tests/data/test_repo.json",
    "content": "{\n    \"name\": \"Test Repo\",\n    \"location\": \"www.google.com\",\n    \"source\": \"www.google.com/file.json\",\n    \"path\": \"tarball/pkg/\",\n    \"packages\": [\n        {\n            \"name\": \"esp32-micropython-1.11.0\",\n            \"type\": \"device\",\n            \"sha256sum\": \"abc123\"\n        },\n        {\n            \"name\": \"esp32_LoBo-esp32_LoBo-3.2.24\",\n            \"type\": \"device\",\n            \"sha256sum\": \"123abc\"\n        },\n        {\n            \"name\": \"esp8266-micropython-1.9.4\",\n            \"type\": \"device\",\n            \"sha256sum\": \"1ab2c3\"\n        }\n    ]\n}\n"
  },
  {
    "path": "tests/data/test_source.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\n    <Name>test-repo</Name>\n    <Prefix></Prefix>\n    <Marker></Marker>\n    <MaxKeys>1000</MaxKeys>\n    <IsTruncated>false</IsTruncated>\n    <Contents>\n        <Key>packages/esp32-micropython-1.10.0.tar.gz</Key>\n        <LastModified>2019-07-10T09:16:54.000Z</LastModified>\n        <ETag>&quot;06290f2d047c0a8ffd7f7b79c98e7c33&quot;</ETag>\n        <Size>12819</Size>\n        <StorageClass>STANDARD</StorageClass>\n    </Contents>\n    <Contents>\n        <Key>packages/esp32-micropython-1.11.0.tar.gz</Key>\n        <LastModified>2019-07-10T09:16:54.000Z</LastModified>\n        <ETag>&quot;23987290858e648effa0d150c7fdd323&quot;</ETag>\n        <Size>13754</Size>\n        <StorageClass>STANDARD</StorageClass>\n    </Contents>\n</ListBucketResult>\n"
  },
  {
    "path": "tests/data/test_sources.json",
    "content": "[\n    {\n        \"name\": \"Test Repo\",\n        \"source\": \"https://google.com/repo.json\"\n    }\n]\n"
  },
  {
    "path": "tests/test_checks.py",
    "content": "import subprocess\n\nfrom micropy.project import checks\n\n\ndef test_vscode_ext_min_version(mock_checks, mocker):\n    \"\"\"Test VSCode Extension Template Checks\"\"\"\n    assert checks.vscode_ext_min_version(\"ms-python.python\")\n    assert not checks.vscode_ext_min_version(\"ms-python.python\", min_version=\"2019.9.34911\")\n    mock_subproc = mocker.patch.object(subprocess, \"run\")\n    mock_subproc.side_effect = [Exception]\n    assert checks.vscode_ext_min_version(\"ms-python.python\")\n"
  },
  {
    "path": "tests/test_config.py",
    "content": "import json\n\nimport pytest\nfrom micropy import config\n\n\nclass TestConfig:\n    default = {\"one\": 1, \"two\": 2, \"sub\": {\"items\": [\"foo\", \"bar\"], \"bool\": True}}\n\n    @pytest.fixture\n    def test_config(self, tmp_path):\n        cfg_file = tmp_path / \"conf.json\"\n        conf = config.Config(cfg_file, default=self.default)\n        return conf\n\n    def get_file_data(self, conf):\n        path = conf.source.file_path\n        return json.loads(path.read_text())\n\n    def test_default(self, tmp_path):\n        cfg_file = tmp_path / \"conf.json\"\n        conf = config.Config(cfg_file, default=self.default)\n        # should not create source file until first change\n        assert not cfg_file.exists()\n        # make change\n        conf.set(\"one\", 2)\n        assert cfg_file.exists()\n        assert self.get_file_data(conf) == conf.raw()\n\n    def test_load_from_file(self, tmp_path, utils):\n        cfg_file = tmp_path / \"conf.json\"\n        cfg_file.write_text(json.dumps(self.default))\n        conf = config.Config(cfg_file, default={})\n        assert conf.config == self.default\n        # default should be overridden\n        conf = config.Config(cfg_file, default={\"one\": 1})\n        assert utils.dict_equal(conf.raw(), self.default)\n\n    def test_override(self, test_config, tmp_path):\n        conf = test_config\n        new_cfg = tmp_path / \"newcfg.json\"\n        conf.source = new_cfg\n        assert isinstance(conf.source, config.JSONConfigSource)\n        diff_cfg = tmp_path / \"diffcfg.json\"\n        conf.source.file_path = diff_cfg\n        conf.sync()\n        assert self.get_file_data(conf) == conf.config\n\n    def test_get(self, test_config):\n        conf = test_config\n        assert conf.get(\"one\") == 1\n        assert conf.get(\"sub/bool\")\n        assert conf.get(\"sub/items/0\") == \"foo\"\n        assert conf.get(\"sub/items\") == [\"foo\", \"bar\"]\n\n    def test_set(self, test_config):\n        conf = test_config\n        conf.set(\"one\", 1)\n        conf.set(\"one/sub/items.0\", \"foobar\")\n        data = json.loads(conf.source.file_path.read_text())\n        assert data == conf.config\n\n    def test_update_from_file(self, test_config):\n        conf = test_config\n        cfg_file = conf.source.file_path\n        new = self.default.copy()\n        new[\"one\"] = 45\n        new[\"section\"] = {\"value\": \"foo\"}\n        cfg_file.write_text(json.dumps(new))\n        conf = config.Config(cfg_file, default=self.default)\n        assert conf.get(\"one\") == 45\n        assert conf.get(\"section/value\") == \"foo\"\n        assert conf.config == new\n\n    def test_extend(self, test_config):\n        conf = test_config\n        conf.extend(\"sub/items\", [\"foobar\", \"barfoo\"])\n        file_data = json.loads(conf.source.file_path.read_text())\n        print(file_data)\n        assert file_data[\"sub\"][\"items\"] == [\"foo\", \"bar\", \"foobar\", \"barfoo\"]\n        assert conf.get(\"sub/items\") == [\"foo\", \"bar\", \"foobar\", \"barfoo\"]\n\n    def test_upsert(self, test_config):\n        conf = test_config\n        conf.upsert(\"sub/items\", [\"barfoo\", \"foobar\", \"bar\", \"foo\"])\n        file_data = self.get_file_data(conf)\n        assert file_data[\"sub\"][\"items\"] == [\"barfoo\", \"foobar\", \"bar\", \"foo\"]\n        assert conf.get(\"sub/items\") == [\"barfoo\", \"foobar\", \"bar\", \"foo\"]\n\n    def test_dict(self):\n        conf = config.Config(source_format=config.DictConfigSource, default=self.default)\n        assert conf.get(\"one\") == 1\n        conf.set(\"sub/bool\", False)\n        assert not conf.get(\"sub/bool\")\n"
  },
  {
    "path": "tests/test_highlevel.py",
    "content": "import json\nimport shutil\nfrom copy import deepcopy\nfrom pathlib import Path\n\nimport pytest\nfrom micropy import project\n\n\n@pytest.fixture\ndef mock_requests(mocker, requests_mock, test_archive):\n    mock_source = {\n        \"name\": \"Micropy Stubs\",\n        \"location\": \"https://codeload.github.com/BradenM/micropy-stubs\",\n        \"source\": \"https://raw.githubusercontent.com/bradenm/micropy-stubs/source.json\",\n        \"path\": \"legacy.tar.gz/pkg/\",\n        \"packages\": [\n            {\n                \"name\": \"micropython\",\n                \"type\": \"firmware\",\n                \"sha256sum\": \"7ff2cce0237268cd52164b77b6c2df6be6249a67ee285edc122960af869b8ed2\",\n            },\n        ],\n    }\n    requests_mock.get(\n        \"https://raw.githubusercontent.com/BradenM/micropy-stubs/master/source.json\",\n        json=mock_source,\n    )\n    requests_mock.get(\n        \"https://codeload.github.com/BradenM/micropy-stubs/legacy.tar.gz/pkg/micropython\",\n        content=test_archive,\n    )\n\n\n@pytest.mark.skip(reason=\"Tests need some serious cleanup before something like this could work.\")\n@pytest.mark.usefixtures(\"mock_requests\")\nclass TestCreateProject:\n    mp = None\n\n    expect_mp_data = staticmethod(\n        lambda name: {\n            \"name\": \"NewProject\",\n            \"stubs\": {name: \"1.2.0\"},\n            \"packages\": {},\n            \"dev-packages\": {\"micropy-cli\": \"*\"},\n            \"config\": {\"vscode\": True, \"pylint\": True},\n        }\n    )\n\n    expect_vsc_data = staticmethod(\n        lambda name: [\n            str(Path(f\".micropy/{name}/frozen\")),\n            str(Path(\".micropy/fware_test_stub/frozen\")),\n            str(Path(f\".micropy/{name}/stubs\")),\n            str(Path(\".micropy/NewProject\")),\n        ]\n    )\n\n    def build_project(self, mpy, path):\n        proj_path = path / \"highlevel_new_project\"\n        if proj_path.exists():\n            shutil.rmtree(proj_path, ignore_errors=True)\n        proj = project.Project(proj_path)\n        proj_stub = list(mpy.stubs)[0]\n        proj.add(project.modules.StubsModule, mpy.stubs, stubs=[proj_stub])\n        proj.add(project.modules.PackagesModule, \"requirements.txt\")\n        proj.add(project.modules.DevPackagesModule, \"dev-requirements.txt\")\n        proj.add(project.modules.TemplatesModule, (\"vscode\", \"pylint\"))\n        return (proj, mpy, proj_stub)\n\n    def check_mp_data(self, path, utils, name=\"esp32\", expect=None):\n        expect_data = expect or self.expect_mp_data(name)\n        micropy_file = path\n        assert micropy_file.exists()\n        mp_data = json.loads(micropy_file.read_text())\n        assert utils.dict_equal(mp_data, expect_data)\n\n    def check_vscode(self, path, name=\"esp32\", expect=None):\n        vsc_path = path / \".vscode\" / \"settings.json\"\n        assert vsc_path.exists()\n        with vsc_path.open() as f:\n            lines = [line.strip() for line in f.readlines() if line]\n            valid = [line for line in lines if \"//\" not in line[:2]]\n        vsc_data = json.loads(\"\\n\".join(valid))\n        expect_data = expect or self.expect_vsc_data(name)\n        assert vsc_data[\"python.analysis.typeshedPaths\"] == expect_data\n\n    def test_setup_stubs(self, mock_micropy, get_stub_paths, shared_datadir):\n        mpy = mock_micropy\n        stub_path = shared_datadir / \"esp32_test_stub\"\n        mpy.stubs.add(stub_path)\n\n    def test_create_project(self, micropy_stubs, tmp_path, utils):\n        proj, mpy, proj_stub = self.build_project(micropy_stubs(), tmp_path)\n        proj.create()\n        self.check_mp_data(proj.info_path, utils, name=proj_stub.path.name)\n        self.check_vscode(proj.path, name=proj_stub.path.name)\n\n    def test_add_package(self, mock_pkg, micropy_stubs, tmp_path, utils):\n        proj, mpy, proj_stub = self.build_project(micropy_stubs(), tmp_path)\n        proj.create()\n        proj.add_package(\"newpackage\")\n        expect_data = deepcopy(self.expect_mp_data(proj_stub.path.name))\n        expect_data[\"packages\"][\"newpackage\"] = \"*\"\n        self.check_mp_data(proj.info_path, utils, expect=expect_data)\n\n    @pytest.mark.parametrize(\"local_pkg\", [\"src/lib/coolpackage\", \"/tmp/absolute/package\"])\n    def test_add_local_package(self, tmp_path, local_pkg, micropy_stubs, utils):\n        proj, mpy, proj_stub = self.build_project(micropy_stubs(), tmp_path)\n        proj.create()\n        local_package = Path(local_pkg)\n        if not local_package.is_absolute():\n            local_package = proj.path / Path(local_pkg)\n        local_package.mkdir(parents=True, exist_ok=True)\n        (local_package / \"__init__.py\").touch()\n        local_path = utils.str_path(local_pkg)\n        proj.add_package(f\"-e {local_path}\")\n        # check micropy.json\n        expect_data = deepcopy(self.expect_mp_data(proj_stub.path.name))\n        expect_data[\"packages\"][local_package.name] = f\"-e {local_path}\"\n        self.check_mp_data(proj.info_path, utils, expect=expect_data)\n        # check vscode settings\n        expect_vscode = deepcopy(self.expect_vsc_data(proj_stub.path.name))\n        expect_vscode.append(local_path)\n        self.check_vscode(proj.path, expect=expect_vscode)\n        shutil.rmtree(proj.path)\n"
  },
  {
    "path": "tests/test_main.py",
    "content": "import micropy.exceptions as exc\nimport pytest\nfrom micropy import data, main\n\n\ndef test_setup(mock_micropy_path):\n    \"\"\"Tests MicroPy Initial Setup\"\"\"\n    expect_mp_dir = mock_micropy_path\n    expect_stubs_dir = mock_micropy_path / \"stubs\"\n    config = main.MicroPyOptions(root_dir=mock_micropy_path)\n    mp = main.MicroPy(options=config)\n    assert expect_mp_dir.exists()\n    assert expect_stubs_dir.exists()\n    # Test after initial setup\n    mp_ = main.MicroPy(options=config)\n    assert len(mp_.stubs) == len(mp.stubs)\n\n\ndef test_add_stub(mock_micropy, shared_datadir):\n    \"\"\"Test Adding Valid Stub\"\"\"\n    fware_path = shared_datadir / \"fware_test_stub\"\n    stub_path = shared_datadir / \"esp8266_test_stub\"\n    stubs = mock_micropy.stubs\n    fware_stub = stubs.add(fware_path, data.STUB_DIR)\n    stub = stubs.add(stub_path, data.STUB_DIR)\n    assert stub in list(mock_micropy.stubs)\n    assert stub.path in data.STUB_DIR.iterdir()\n    assert stub.path.exists()\n    assert fware_stub in list(mock_micropy.stubs._firmware)\n\n\ndef test_stub_error():\n    with pytest.raises(exc.StubError):\n        raise exc.StubError(None)\n\n\ndef test_resolve_project(mocker, mock_micropy):\n    mock_proj = mocker.patch.object(main, \"Project\").return_value\n    mock_proj.exists = False\n    assert not mock_micropy.resolve_project(\".\").exists\n    mock_proj.exists = True\n    assert mock_micropy.resolve_project(\".\")\n"
  },
  {
    "path": "tests/test_packages.py",
    "content": "from pathlib import Path\n\nimport pytest\nfrom boltons.namedutils import namedlist\nfrom micropy import packages\n\nEXPPKG = namedlist(\"ExpectPackage\", [\"name\", \"specs\", \"full_name\"])\n\n\nclass TestPackages:\n    class MockSource:\n        def __init__(self, pkg, has_init):\n            self.pkg = pkg\n            self.has_init = has_init\n\n    @pytest.fixture(params=[True, False], ids=[\"package\", \"module\"])\n    def mock_source(self, request, mock_pkg):\n        if request.param:\n            # true packages vs file\n            (mock_pkg / \"__init__.py\").touch()\n        return self.MockSource(mock_pkg, request.param)\n\n    @pytest.fixture(params=[True, False], ids=[\"package\", \"module\"])\n    def mock_source_path(self, request, tmp_path):\n        path = tmp_path / \"file.py\"\n        if request.param is True:\n            pkg = tmp_path\n            path = tmp_path / \"__init__.py\"\n        else:\n            pkg = tmp_path\n        path.touch()\n        return self.MockSource(pkg, request.param)\n\n    @pytest.mark.parametrize(\n        \"package,expect\",\n        [\n            (\n                \"git+https://github.com/jczic/MicroWebSrv2.git@master#egg=MicroWebSrv2\",\n                packages.VCSDependencySource,\n            ),\n            (\"picoweb\", packages.PackageDependencySource),\n            (\"-e /foobar/pkg\", packages.LocalDependencySource),\n        ],\n    )\n    def test_factory(self, package, expect):\n        source = packages.create_dependency_source(package)\n        assert isinstance(source, expect)\n\n    @pytest.mark.parametrize(\n        \"requirement,expect\",\n        [\n            (\n                [\"git+https://github.com/jczic/MicroWebSrv2.git@master#egg=MicroWebSrv2\"],\n                [\n                    \"microwebsrv2\",\n                    \"git+https://github.com/jczic/MicroWebSrv2.git@master#egg=MicroWebSrv2\",\n                    \"git+https://github.com/jczic/MicroWebSrv2.git@master#egg=MicroWebSrv2\",\n                ],\n            ),\n            ([\"picoweb\"], [\"picoweb\", \"*\", \"picoweb\"]),\n            ([\"picoweb==^7.1\"], [\"picoweb\", \"==^7.1\", \"picoweb==^7.1\"]),\n            ([\"BlynkLib==0.0.0\"], [\"blynklib\", \"==0.0.0\", \"blynklib==0.0.0\"]),\n            (\n                [\"-e /foobar/somepkg\", \"somepackage\"],\n                [\"somepackage\", \"-e /foobar/somepkg\", \"-e /foobar/somepkg\"],\n            ),\n            ([\"-e /foobar/somepkg\"], [\"somepkg\", \"-e /foobar/somepkg\", \"-e /foobar/somepkg\"]),\n        ],\n    )\n    def test_package(self, mock_pkg, requirement, expect):\n        source = packages.create_dependency_source(*requirement)\n        pkg = source.package\n        assert pkg.name == expect[0]  # name\n        assert pkg.pretty_specs == expect[1]  # specs\n        assert str(pkg) == expect[2]  # full name ()\n\n    def test_package_source(self, mock_source):\n        def format_desc(x):\n            return f\"desc{x}\"\n\n        source = packages.create_dependency_source(\"blynklib\", format_desc=format_desc)\n        with source as files:\n            if mock_source.has_init:\n                assert isinstance(files, Path)\n                assert mock_source.pkg.name == files.name\n            else:\n                assert isinstance(files, list)\n                file_names = [(p.name, s.name) for p, s in files]\n                file_names = list(sum(file_names, ()))\n                assert sorted([\"file.py\", \"file.pyi\", \"module.py\", \"module.pyi\"]) == sorted(\n                    file_names\n                )\n\n    def test_package_path(self, mock_source_path):\n        source = packages.create_dependency_source(f\"-e {mock_source_path.pkg}\")\n        with source as files:\n            if mock_source_path.has_init:\n                # is proper package\n                assert files.is_dir()\n            if mock_source_path.has_init is False:\n                # is module\n                assert files.is_dir()\n                assert files == source.package.path\n\n    @pytest.mark.parametrize(\n        \"pkg,expect\",\n        [\n            ((\"micropy-cli\", \"*\"), EXPPKG(\"micropy-cli\", \"*\", \"micropy-cli\")),\n            ((\"blynklib\", \"==0.0.0\"), EXPPKG(\"blynklib\", \"==0.0.0\", \"blynklib==0.0.0\")),\n            (\n                (\"custompkg\", \"-e src/lib/custompackage\"),\n                EXPPKG(\"custompkg\", \"-e src/lib/custompackage\", \"-e src/lib/custompackage\"),\n            ),\n        ],\n    )\n    def test_package_from_text(self, pkg, expect, utils):\n        pkg = packages.Package.from_text(*pkg)\n        assert pkg.name == expect.name\n        assert pkg.full_name == pkg.full_name\n        assert pkg.pretty_specs == expect.specs\n        is_e = \"-e\" in pkg.full_name\n        assert pkg.editable == is_e\n        if pkg.editable:\n            assert pkg.path == Path(\"src/lib/custompackage\")\n        else:\n            assert pkg.path is None\n"
  },
  {
    "path": "tests/test_project.py",
    "content": "import shutil\n\nimport pytest\nfrom boltons import setutils\nfrom micropy import config, packages, project\nfrom micropy.exceptions import RequirementException\nfrom micropy.project import modules\nfrom requests import RequestException\n\n\n@pytest.fixture\ndef get_module(tmp_path):\n    def _get_module(names, mp, **kwargs):\n        _templates = list(modules.TemplatesModule.TEMPLATES.keys())\n        mods = {\n            \"stubs\": (\n                (\n                    modules.StubsModule,\n                    mp.stubs,\n                ),\n                {\"stubs\": list(mp.stubs)[:2]},\n            ),\n            \"template\": ((modules.TemplatesModule, _templates), {}),\n            \"reqs\": ((modules.PackagesModule, \"requirements.txt\"), {}),\n            \"dev-reqs\": ((modules.DevPackagesModule, \"dev-requirements.txt\"), {}),\n        }\n        if names == \"all\":\n            names = \",\".join(list(mods.keys()))\n        _mods = [mods[n.strip()] for n in names.split(\",\") if n]\n        yield from _mods\n\n    return _get_module\n\n\n@pytest.fixture\ndef get_config():\n    def _get_config(request, name=\"NewProject\", stubs=None, templates=None, packages=None):\n        packages = packages or dict()\n        templates = templates or [\"vscode\", \"pylint\"]\n        stubs = stubs or []\n        _mods = {\n            \"base\": {\n                \"name\": name,\n            },\n            \"stubs\": {\"stubs\": {s.name: s.stub_version for s in stubs}},\n            \"template\": {\"config\": {t: (t in templates) for t in templates}},\n            \"reqs\": {\"packages\": packages.get(\"reqs\", {})},\n            \"dev-reqs\": {\"dev-packages\": packages.get(\"dev-reqs\", {\"micropy-cli\": \"*\"})},\n        }\n        if request == \"all\":\n            request = \",\".join(list(_mods.keys()))\n        mods = request.split(\",\")\n        test_config = _mods[\"base\"].copy()\n        for m in mods:\n            test_config = {**test_config, **_mods[m or \"base\"]}\n        return test_config\n\n    return _get_config\n\n\n@pytest.fixture\ndef get_context():\n    def _get_context(request, stubs=None, pkg_path=None, data_dir=None):\n        stubs = stubs or []\n        _frozen = [s.frozen for s in stubs]\n        _fware = [s.firmware.frozen for s in stubs if s.firmware is not None]\n        _stub_paths = [s.stubs for s in stubs]\n        _paths = setutils.IndexedSet([*_frozen, *_fware, *_stub_paths])\n        _context = {\n            \"base\": {},\n            \"stubs\": {\n                \"stubs\": set(stubs),\n                \"paths\": _paths,\n                \"datadir\": data_dir,\n            },\n            \"reqs\": {\"paths\": setutils.IndexedSet([pkg_path]), \"local_paths\": set()},\n        }\n        if request == \"all\":\n            request = \",\".join(list(_context.keys()))\n        mods = request.split(\",\")\n        if \"reqs\" in mods and \"stubs\" in mods:\n            _ctx = _context[\"stubs\"].copy()\n            _ctx[\"paths\"].update(_context[\"reqs\"][\"paths\"])\n            _ctx[\"local_paths\"] = _context[\"reqs\"][\"local_paths\"]\n            return _ctx\n        context = {}\n        for m in mods:\n            context = {**context, **_context.get(m, {})}\n        return context\n\n    return _get_context\n\n\n@pytest.fixture\ndef test_project(micropy_stubs, tmp_path, get_module):\n    def _test_project(mods=\"\", path=None):\n        mp = micropy_stubs()\n        proj_path = path if path else tmp_path / \"NewProject\"\n        proj = project.Project(proj_path)\n        mods = get_module(mods, mp)\n        for m in mods:\n            proj.add(*m[0], **m[1])\n        yield proj, mp\n\n    return _test_project\n\n\n@pytest.fixture\ndef tmp_project(tmp_path, shared_datadir):\n    path = shared_datadir / \"project_test\"\n    proj_path = tmp_path / \"NewProject\"\n    shutil.copytree(path, proj_path)\n    return proj_path\n\n\ndef test_implementation(mocker):\n    \"\"\"Test Abstract Base Class\"\"\"\n    mocker.patch.object(modules.ProjectModule, \"__abstractmethods__\", new_callable=set)\n    inst = modules.ProjectModule()\n    inst.config\n    inst.load()\n    inst.create()\n    inst.update()\n    inst.add([])\n    inst.remove([])\n\n\ndef test_project_queue(tmp_path, mock_cwd, mocker):\n    mocker.patch(\"micropy.project.project.Config\")\n    path = tmp_path / \"project_path\"\n    proj = project.Project(path)\n    # should create/load projects based on <MODULES>.PRIORITY,\n    # not based on order added\n    mock_parent = mocker.Mock()\n    mock_parent.m1.return_value = mocker.Mock(PRIORITY=8)\n    mock_parent.m2.return_value = mocker.Mock(PRIORITY=0)\n    mock_parent.m3.return_value = mocker.Mock(PRIORITY=9)\n    proj.add(mock_parent.m1)\n    proj.add(mock_parent.m2)\n    proj.add(mock_parent.m3)\n    proj.create()\n    # should be called in order of priority (9 being highest)\n    mock_parent.assert_has_calls(\n        [\n            # adding them doesn't matter\n            mocker.call.m1(log=mocker.ANY, parent=mocker.ANY),\n            mocker.call.m2(log=mocker.ANY, parent=mocker.ANY),\n            mocker.call.m3(log=mocker.ANY, parent=mocker.ANY),\n            # create order should differ\n            mocker.call.m3().create(),\n            mocker.call.m1().create(),\n            mocker.call.m2().create(),\n        ]\n    )\n\n\n@pytest.mark.parametrize(\"mods\", [\"\", \"stubs\", \"template\", \"reqs\", \"dev-reqs\", \"all\"])\nclass TestProject:\n    def test_create(self, test_project, mock_checks, mods, utils):\n        test_proj, _ = next(test_project(mods))\n        assert test_proj.config.get(\"name\") == \"NewProject\"\n        resp = test_proj.create()\n        assert str(resp) == utils.str_path(test_proj.path)\n        assert test_proj.exists\n        assert test_proj.info_path.exists()\n        if test_proj._children:\n            test_proj.remove(type(test_proj._children[-1]))\n\n    def test_config(self, test_project, get_config, mods, utils):\n        test_proj, mp = next(test_project(mods))\n        expect_config = get_config(mods, stubs=list(mp.stubs)[:2])\n        assert test_proj.config._config == {\"name\": \"NewProject\"}\n        test_proj.create()\n        assert utils.dict_equal(test_proj.config.raw(), expect_config)\n        # should be the same post-load\n        test_proj.load()\n        assert utils.dict_equal(test_proj.config.raw(), expect_config)\n\n    def test_context(self, test_project, get_context, mods, tmp_path, utils):\n        proj_path = tmp_path / \"tmpprojpath\"\n        test_proj, mp = next(test_project(mods, path=proj_path))\n        test_proj.create()\n        pkg_path = test_proj.data_path / test_proj.name\n        expect_context = get_context(\n            mods, stubs=mp.stubs, pkg_path=pkg_path, data_dir=test_proj.data_path\n        )\n        assert utils.dict_equal(test_proj.context.raw(), expect_context)\n        test_proj.load()  # should be the same post load\n        assert utils.dict_equal(test_proj.context.raw(), expect_context)\n\n    def test_load(self, mock_pkg, tmp_project, mock_checks, test_project, mods, utils):\n        proj, mp = next(test_project(mods, path=tmp_project))\n        proj.load(run_checks=mp.RUN_CHECKS)\n\n    def test_update(self, mock_pkg, tmp_project, mock_checks, test_project, mods, utils):\n        proj, mp = next(test_project(mods, path=tmp_project))\n        proj.update()\n\n\nclass TestStubsModule:\n    @pytest.fixture()\n    def stub_module(self, mocker, tmp_path, micropy_stubs):\n        mp = micropy_stubs()\n        parent_mock = mocker.MagicMock()\n        parent_mock.data_path = tmp_path / \".micropy\"\n        stub_item = list(mp.stubs)[0]\n        stub_mod = modules.StubsModule(\n            mp.stubs, stubs=[stub_item], parent=parent_mock, log=mocker.Mock()\n        )\n        return stub_mod, mp\n\n    def test_load(self, tmp_project, stub_module, get_stub_paths):\n        custom_stub = next(get_stub_paths())\n        stub_mod, mp = stub_module\n        stub_data = {\n            \"esp32-micropython-1.11.0\": \"1.2.0\",\n            \"esp8266-micropython-1.11.0\": \"1.2.0\",\n            \"custom-stub\": str(custom_stub),\n        }\n        stub_mod.parent.config.get.return_value = stub_data\n        stub_mod.stub_manager.add.return_value = mp.stubs\n        assert stub_mod.load(stub_data=stub_data)\n\n    def test_add_stub(self, test_project, get_stub_paths, mocker):\n        proj, mp = next(test_project(\"stubs\"))\n        proj.create()\n        mocker.patch.object(config.config.dpath, \"merge\")\n        stub_path = next(get_stub_paths())\n        stub = mocker.MagicMock()\n        stub.path = stub_path\n        stub.frozen = stub_path / \"frozen\"\n        stub.stubs = stub_path / \"stubs\"\n        stub.firmware = stub\n        proj.add_stub(stub)\n        print(proj.stubs)\n\n\nclass TestPackagesModule:\n    @pytest.fixture\n    def test_package(self, mocker, tmp_path, mock_pkg, test_project):\n        new_path = tmp_path / \"somepath\"\n        proj, mp = next(test_project(\"reqs\", path=new_path))\n        proj.create()\n        return proj\n\n    @pytest.mark.flaky\n    def test_add_package(self, test_project, mock_pkg, tmp_path):\n        proj, mp = next(test_project(\"reqs\"))\n        proj.create()\n        proj.add_package(\"somepkg==7\")\n        # Shouldn't allow duplicate pkgs\n        res = proj.add_package(\"somepkg\")\n        assert res is None\n\n    @pytest.mark.flaky()\n    def test_add_local_package(self, test_project, tmp_path, utils):\n        proj, mp = next(test_project(\"reqs\"))\n        pkg = tmp_path / \"custompkg\"\n        pkg.mkdir(parents=True)\n        # Add from path\n        proj.add_package(f\"-e {pkg}\")\n\n    def test_package_error(self, test_project, mock_pkg, mocker, tmp_path, caplog):\n        packages.source_package.utils.ensure_valid_url.side_effect = [RequestException]\n        path = tmp_path / \"newdir\"\n        proj, mp = next(test_project(\"reqs\", path=path))\n        proj.create()\n        with pytest.raises(RequirementException):\n            pkgs = proj.add_package(\"newpackage\")\n            assert proj.config.get(\"packages/newpackage\", None) is None\n            assert \"newpackage\" not in pkgs.keys()\n\n    def test_add_dev_package(self, mocker, mock_pkg, test_project):\n        proj, mp = next(test_project(\"reqs,dev-reqs\"))\n        proj.create()\n        proj.add_package(\"somepkg\")\n        proj.add_package(\"anotha_pkg\", dev=True)\n"
  },
  {
    "path": "tests/test_pyd.py",
    "content": "from __future__ import annotations\n\nimport hashlib\nimport stat\nimport sys\nfrom pathlib import PurePosixPath\nfrom typing import Literal, Type\nfrom unittest.mock import ANY, MagicMock\n\nimport pytest\nfrom micropy.exceptions import PyDeviceError\nfrom micropy.pyd import backend_rshell, backend_upydevice, consumers\nfrom micropy.pyd.abc import DevicePath, MetaPyDeviceBackend, PyDeviceConsumer\nfrom micropy.pyd.pydevice import PyDevice\nfrom pytest_mock import MockFixture\n\n\n@pytest.fixture\ndef mock_upy(mocker: MockFixture):\n    mock_upy = mocker.patch.object(backend_upydevice, \"upydevice\", autospec=True)\n    return mock_upy\n\n\n@pytest.fixture\ndef mock_upy_uos(mocker: MockFixture):\n    mock_uos = mocker.patch.object(backend_upydevice, \"UOS\", autospec=True)\n    return mock_uos\n\n\n@pytest.fixture\ndef mock_upy_retry(mocker: MockFixture):\n    mock_retry = mocker.patch.object(backend_upydevice, \"retry\", autospec=True)\n    return mock_retry\n\n\n@pytest.fixture\ndef mock_rsh(mocker: MockFixture):\n    mock_rsh = mocker.patch.object(backend_rshell, \"rsh\", autospec=True)\n    mock_rsh.connect = mocker.Mock()\n    mock_rsh.find_serial_device_by_port = mocker.Mock()\n    mock_rsh.cp = mocker.Mock()\n    return mock_rsh\n\n\nclass MockAdapter:\n    backend: Literal[\"upy\", \"rsh\"]\n    mock: MagicMock\n    mock_uos: MagicMock\n\n    def __init__(self, backend: Literal[\"upy\", \"rsh\"], mock: MagicMock, mock_uos=None):\n        self.backend = backend\n        self.mock = mock\n        if mock_uos:\n            self.mock_uos = mock_uos\n\n    @property\n    def is_rsh(self) -> bool:\n        return self.backend == \"rsh\"\n\n    @property\n    def is_upy(self) -> bool:\n        return self.backend == \"upy\"\n\n    @property\n    def connect(self) -> MagicMock:\n        return self.mock.Device.return_value.connect if self.is_upy else self.mock.connect\n\n    @property\n    def device(self):\n        return self.mock.Device.return_value if self.is_upy else self.mock\n\n\nMOCK_PORT = \"/dev/port\"\n\nIS_WIN_PY310 = sys.version_info >= (3, 10) and sys.platform.startswith(\"win\")\n\n\n@pytest.fixture(params=[True, False])\ndef with_consumer(request: pytest.FixtureRequest, mocker: MockFixture):\n    if request.param is True:\n        consumer_mock = mocker.MagicMock(PyDeviceConsumer)\n        return dict(consumer=consumer_mock)\n    return dict()\n\n\nclass TestPyDeviceBackend:\n    backend: Literal[\"upy\", \"rsh\"]\n    pyd_cls: Type[MetaPyDeviceBackend]\n\n    @pytest.fixture(\n        params=[\n            \"upy\",\n            pytest.param(\n                \"rsh\",\n                marks=pytest.mark.skipif(\n                    IS_WIN_PY310,\n                    reason=\"skipping due to rshell/pyreadline broken for >=py310 on windows.\",\n                ),\n            ),\n        ]\n    )\n    def pymock_setup(self, request: pytest.FixtureRequest):\n        self.backend = request.param\n        self.pyd_cls = (\n            backend_upydevice.UPyDeviceBackend\n            if self.backend == \"upy\"\n            else backend_rshell.RShellPyDeviceBackend\n        )\n\n    @pytest.fixture\n    def pymock(self, pymock_setup, request: pytest.FixtureRequest, mock_upy_uos):\n        mod_mock = request.getfixturevalue(f\"mock_{self.backend}\")\n        m = MockAdapter(self.backend, mod_mock, mock_upy_uos)\n        yield m\n        m.mock.reset_mock()\n\n    def test_init(self, pymock):\n        m = pymock\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        if self.backend == \"upy\":\n            m.mock.Device.assert_called_once_with(MOCK_PORT, init=True, autodetect=True)\n        else:\n            assert m.mock.ASCII_XFER is False\n            assert m.mock.QUIET is True\n        assert pyd.location == MOCK_PORT\n\n    def test_init__connect_fail(self, pymock):\n        m = pymock\n        m.connect.side_effect = [SystemExit, SystemExit]\n        with pytest.raises(PyDeviceError):\n            self.pyd_cls().establish(MOCK_PORT).connect()\n\n    def test_connected__default(self, pymock):\n        m = pymock\n        pyd = self.pyd_cls()\n        assert not pyd.connected\n        if m.is_upy:\n            with pytest.raises(PyDeviceError):\n                pyd._ensure_connected()\n\n    def test_connected(self, pymock):\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        pyd.connect()\n        assert pyd.connected\n\n    def test_disconnect(self, pymock):\n        m = pymock\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        pyd.disconnect()\n        if m.is_upy:\n            m.mock.Device.return_value.disconnect.assert_called_once()\n\n    def test_reset(self, pymock, mocker: MockFixture):\n        mocker.patch(\"time.sleep\")\n        m = pymock\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        pyd.reset()\n        if m.is_upy:\n            m.device.reset.assert_called_once()\n            m.device.connect.assert_called_once()\n\n    def test_eval(self, pymock, mocker: MockFixture, with_consumer):\n        m = pymock\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        pyd.connect()\n        pyd._pydevice.exec_raw = mocker.Mock(return_value=[\"\", \"\"])\n        pyd.eval(\"abc\", **with_consumer)\n        has_cons = \"consumer\" in with_consumer\n        if m.is_upy:\n            m.device.cmd.assert_called_once_with(\"abc\", follow=True, pipe=mocker.ANY)\n        else:\n            if has_cons:\n                pyd._pydevice.exec_raw.assert_called_once_with(\"abc\", data_consumer=mocker.ANY)\n            else:\n                pyd._pydevice.exec_raw.assert_called_once_with(\"abc\", data_consumer=None)\n\n    def test_eval_script(self, pymock, mocker: MockFixture, with_consumer):\n        m = pymock\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        pyd.connect()\n        pyd._pydevice.exec_raw = mocker.Mock(return_value=[b\"\", b\"\"])\n        pyd.eval_script(b\"import something\", \"somefile.py\", **with_consumer)\n        if m.is_upy:\n            if \"consumer\" in with_consumer:\n                with_consumer[\"consumer\"].on_start.assert_called_once()\n            m.device.cmd.assert_any_call(\"import ubinascii\")\n            mock_random = mocker.patch(\"random.sample\", return_value=\"abc.py\")\n            pyd = self.pyd_cls().establish(MOCK_PORT)\n            pyd.connect()\n            pyd.eval_script(b\"import something\", None, **with_consumer)\n            mock_random.assert_called_once()\n\n    @property\n    def read_file_effects(self):\n        cmd_effects = [\n            None,  # import ubin\n            None,  # open file\n            8,  # content size\n            0,  # seek start\n            0,  # pos\n            b\"Hi there\",  # read,\n            8,  # pos\n            None,  # close\n        ]\n        return cmd_effects\n\n    def test_read_file(self, mock_upy_retry, pymock):\n        m = pymock\n        if m.is_rsh:\n            # upy only\n            return\n        # content size\n        m.mock_uos.return_value.stat.side_effect = [\"ENOENT\", [0, 0, 0, 0, 0, 0, 8]]\n        # chunk size will default to 8/4\n        m.device.cmd.side_effect = [8, b\"Hi\", b\" t\", b\"he\", b\"re\"]\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        res = pyd.read_file(\"/some/path\", verify_integrity=False)\n        assert res == \"Hi there\"\n        mock_upy_retry.assert_not_called()\n        assert m.device.cmd.call_count == 5\n\n    def test_read_file__with_integrity(self, mock_upy_retry, pymock):\n        m = pymock\n        if m.is_rsh:\n            # upy only\n            return\n        # content size\n        m.mock_uos.return_value.stat.side_effect = [\"ENOENT\", [0, 0, 0, 0, 0, 0, 8]]\n        # chunk size will default to 8/4\n        chunks = [b\"Hi\", b\" t\", b\"he\", b\"re\"]\n        chunks_hash = hashlib.sha256()\n        for chunk in chunks:\n            chunks_hash.update(chunk)\n        m.device.cmd.side_effect = [8, *chunks, chunks_hash.hexdigest()]\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        res = pyd.read_file(\"/some/path\", verify_integrity=True)\n        assert res == \"Hi there\"\n        mock_upy_retry.assert_not_called()\n        assert m.device.cmd.call_count == 6\n\n    def test_read_file__with_integrity_fail(self, mock_upy_retry, pymock, mocker: MockFixture):\n        m = pymock\n        if m.is_rsh:\n            # upy only\n            return\n        # content size\n        m.mock_uos.return_value.stat.side_effect = [\"ENOENT\", [0, 0, 0, 0, 0, 0, 8]]\n        # chunk size will default to 8/4\n        chunks = [b\"Hi\", b\" t\", b\"he\", b\"re\"]\n        m.device.cmd.side_effect = [8, *chunks, \"notrightsha\"]\n        reset_mock = mocker.MagicMock()\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        pyd.reset = reset_mock\n        pyd.read_file(\"/some/path\", verify_integrity=True)\n        assert reset_mock.call_count == 4\n        assert m.device.cmd.call_count == 6\n\n    def test_read_file__bad_chunk(self, mock_upy_retry, pymock, mocker: MockFixture):\n        m = pymock\n        if m.is_rsh:\n            # upy only\n            return\n        # content size\n        m.mock_uos.return_value.stat.side_effect = [\"ENOENT\", [0, 0, 0, 0, 0, 0, 8]]\n        # chunk size will default to 8/4\n        chunks = [b\"Hi\", b\"\", b\" t\", b\"he\", b\"re\"]\n        m.device.cmd.side_effect = [8, *chunks]\n        m.mock.Device.return_value.reset = mocker.MagicMock(return_value=None)\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        res = pyd.read_file(\"/some/path\", verify_integrity=False)\n        assert res == \"Hi there\"\n        mock_upy_retry.assert_not_called()\n        assert m.device.cmd.call_count == 6\n        assert m.mock.Device.return_value.reset.call_count == 1\n\n    def test_read_file__error_chunk(self, mock_upy_retry, pymock, mocker: MockFixture):\n        m = pymock\n        if m.is_rsh:\n            # upy only\n            return\n        # content size\n        m.mock_uos.return_value.stat.side_effect = [\"ENOENT\", [0, 0, 0, 0, 0, 0, 8]]\n        # chunk size will default to 8/4\n        chunks = [b\"Hi\", RuntimeError, b\" t\", b\"he\", b\"re\"]\n        m.device.cmd.side_effect = [8, *chunks]\n        reset_mock = mocker.MagicMock()\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        pyd.reset = reset_mock\n        pyd.read_file(\"/some/path\", verify_integrity=True)\n        assert reset_mock.call_count == 5\n        assert m.device.cmd.call_count == 4\n\n    def test_pull_file(self, pymock, tmp_path, mock_upy_retry):\n        m = pymock\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        pyd.connect()\n        if m.is_upy:\n            m.mock_uos.return_value.stat.side_effect = [\n                \"ENOENT\",  # resolve root\n                \"ENOENT\",  # resolve root\n                [0, 0, 0, 0, 0, 0, 8],\n            ]\n            # chunk size will default to 8/4\n            m.device.cmd.side_effect = [8, b\"Hi\", b\" t\", b\"he\", b\"re\"]\n            pyd.pull_file(\"/some/path\", (tmp_path / \"out.txt\"), verify_integrity=False)\n            assert (tmp_path / \"out.txt\").read_text() == \"Hi there\"\n        else:\n            m.mock.find_serial_device_by_port.return_value.name_path = \"/\"\n            pyd.pull_file(\"/some/path\", (tmp_path / \"out.txt\"))\n            m.mock.cp.assert_called_once_with(\"/some/path\", str(tmp_path / \"out.txt\"))\n\n    def test_iter_files(self, pymock):\n        m = pymock\n        if m.is_rsh:\n            return\n        pyd = self.pyd_cls().establish(MOCK_PORT)\n        pyd.connect()\n        m.device.cmd.side_effect = [\n            None,\n            None,\n            None,\n            [(\"name\", \"stat\", \"\", \"\")],\n            None,\n            [(\"name\", stat.S_IFDIR, \"\", \"\")],\n            None,\n            [(\"underName\", \"\", \"\", \"\")],\n        ]\n        assert list(pyd.iter_files(\"/some/path\")) == []\n        m.device.cmd.assert_any_call(\"import uos\", silent=True)\n        m.device.cmd.assert_any_call(\"list(uos.ilistdir('/some/path'))\", silent=True, rtn_resp=True)\n        assert list(pyd.iter_files(\"/some/path\")) == [PurePosixPath(\"/some/path/name\")]\n        assert list(pyd.iter_files(\"/some/path\")) == [PurePosixPath(\"/some/path/name/underName\")]\n\n\nclass TestPyDevice:\n    @pytest.fixture\n    def mock_backend(self, mocker: MockFixture):\n        mock = mocker.MagicMock(MetaPyDeviceBackend)\n        mock.return_value.establish.return_value = mock.return_value\n        return mock\n\n    @pytest.fixture(\n        params=[\n            [\"dir\", \"/some/dir\"],\n            [\"file\", \"/some/file.txt\"],\n            [\"dir\", r\"c:\\\\some\\\\dos\\\\dir\"],\n            [\"file\", r\"c:\\\\some\\\\dos\\\\file.txt\"],\n        ]\n    )\n    def path_type(self, request: pytest.FixtureRequest):\n        return request.param\n\n    @pytest.mark.parametrize(\n        \"pyd_kwargs\",\n        [\n            dict(),\n            dict(auto_connect=False),\n            dict(delegate_cls=lambda *x: x, stream_consumer=\"stream\", message_consumer=\"message\"),\n        ],\n    )\n    def test_init(self, mock_backend, pyd_kwargs):\n        pyd = PyDevice(MOCK_PORT, backend=mock_backend, **pyd_kwargs)\n        mock_backend.assert_called_once()\n        mock_backend.return_value.establish.assert_called_once_with(MOCK_PORT)\n        if pyd_kwargs.get(\"auto_connect\", True):\n            mock_backend.return_value.connect.assert_called_once()\n        if \"delegate_cls\" in pyd_kwargs:\n            assert pyd.consumer == (\n                \"stream\",\n                \"message\",\n            )\n\n    def test_connect(self, mock_backend):\n        pyd = PyDevice(MOCK_PORT, backend=mock_backend, auto_connect=False)\n        pyd.connect()\n        mock_backend.return_value.connect.assert_called_once()\n\n    def test_disconnect(self, mock_backend):\n        pyd = PyDevice(MOCK_PORT, backend=mock_backend)\n        pyd.disconnect()\n        mock_backend.return_value.disconnect.assert_called_once()\n\n    def test_copy_from(self, mock_backend, path_type, mocker):\n        pyd = PyDevice(MOCK_PORT, backend=mock_backend)\n        ptype, p = path_type\n        pyd.copy_from(p, \"/host/path\")\n        if ptype == \"dir\":\n            mock_backend.return_value.copy_dir.assert_called_once_with(\n                p,\n                \"/host/path\",\n                consumer=mocker.ANY,\n                verify_integrity=mocker.ANY,\n                exclude_integrity=None,\n            )\n        else:\n            mock_backend.return_value.pull_file.assert_called_once_with(\n                p,\n                \"/host/path\",\n                consumer=mocker.ANY,\n                verify_integrity=mocker.ANY,\n            )\n\n    def test_copy_from__integrity(self, mock_backend, path_type, mocker):\n        pyd = PyDevice(MOCK_PORT, backend=mock_backend)\n        ptype, p = path_type\n        if ptype == \"dir\":\n            pyd.copy_from(p, \"/host/path\", verify_integrity=True, exclude_integrity={\"abc.py\"})\n        else:\n            pyd.copy_from(p, \"/host/path\", verify_integrity=True)\n        if ptype == \"dir\":\n            mock_backend.return_value.copy_dir.assert_called_once_with(\n                p,\n                \"/host/path\",\n                consumer=mocker.ANY,\n                verify_integrity=True,\n                exclude_integrity={\"abc.py\"},\n            )\n        else:\n            mock_backend.return_value.pull_file.assert_called_once_with(\n                p, \"/host/path\", consumer=mocker.ANY, verify_integrity=True\n            )\n\n    def test_copy_to(self, mock_backend, path_type, mocker):\n        pyd = PyDevice(MOCK_PORT, backend=mock_backend)\n        ptype, p = path_type\n        if ptype == \"dir\":\n            with pytest.raises(RuntimeError):\n                pyd.copy_to(\"/host/path\", p)\n        else:\n            pyd.copy_to(\"/host/path/f.txt\", p)\n            mock_backend.return_value.push_file.assert_called_once_with(\n                \"/host/path/f.txt\", p, consumer=mocker.ANY\n            )\n\n    def test_remove(self, mock_backend):\n        pyd = PyDevice(MOCK_PORT, backend=mock_backend)\n        pyd.remove(DevicePath(\"/some/path\"))\n        mock_backend.return_value.remove.assert_called_once_with(\"/some/path\")\n\n\nclass TestConsumers:\n    @pytest.mark.parametrize(\n        \"on_desc,expected_tqdm\",\n        [\n            [None, dict(total=5, unit=\"B\", unit_scale=True, unit_divisor=1024, bar_format=ANY)],\n            [\n                lambda n, cfg: (\"other\", dict(override=True)),\n                dict(\n                    total=5,\n                    unit=\"B\",\n                    unit_scale=True,\n                    unit_divisor=1024,\n                    bar_format=ANY,\n                    override=True,\n                ),\n            ],\n        ],\n    )\n    def test_progress_consumer(self, mocker: MockFixture, on_desc, expected_tqdm):\n        tqdm_mock = mocker.patch.object(consumers, \"tqdm\")\n        cons = consumers.ProgressStreamConsumer(on_desc)\n        cons.on_start(name=\"abc\", size=5)\n        tqdm_mock.assert_called_once_with(**expected_tqdm)\n        cons.on_update(size=1)\n        tqdm_mock.return_value.update.assert_called_once_with(1)\n        cons.on_end()\n        tqdm_mock.return_value.close.assert_called_once()\n\n    def test_delegate(self):\n        delegate = consumers.ConsumerDelegate()\n        assert delegate.on_message(\"\") is None\n        delegate = consumers.ConsumerDelegate(consumers.MessageHandlers(on_message=lambda m: m))\n        assert delegate.on_message(\"a\") == \"a\"\n\n    @pytest.mark.skipif(\n        IS_WIN_PY310, reason=\"skipping due to rshell/pyreadline broken for >=py310 on windows.\"\n    )\n    def test_rsh_consumer(self):\n        calls = []\n        message_cons = consumers.MessageHandlers(on_message=lambda m: calls.append(m))\n        rsh_cons = backend_rshell.RShellConsumer(message_cons.on_message)\n        chars = iter(list(\"a line.\\nnext line.\\n\"))\n        for i in chars:\n            rsh_cons.on_message(i.encode())\n        assert len(calls) == 2\n        assert calls == [\"a line.\", \"next line.\"]\n"
  },
  {
    "path": "tests/test_stub_source.py",
    "content": "from micropy.stubs import source\n\nfrom tests.test_stubs_repo import stub_repo  # noqa\n\n\ndef test_stub_info_spec_locator(shared_datadir):\n    test_path = shared_datadir / \"esp8266_test_stub\"\n    assert source.StubInfoSpecLocator().prepare(test_path) == test_path.absolute()\n\n\ndef test_stub_info_spec_locator__returns_location_on_fail(tmp_path):\n    assert source.StubInfoSpecLocator().prepare(tmp_path) == tmp_path\n\n\ndef test_source_ready(shared_datadir, test_urls, tmp_path, mocker, test_archive):\n    \"\"\"should prepare and resolve stub\"\"\"\n    # Test LocalStub ready\n    test_path = shared_datadir / \"esp8266_test_stub\"\n    local_stub = source.get_source(test_path)\n    expected_path = local_stub.location.resolve()\n    with local_stub.ready() as source_path:\n        assert source_path == expected_path\n\n    # Setup RemoteStub\n    test_parent = tmp_path / \"tmpdir\"\n    test_parent.mkdir()\n    expected_path = (test_parent / \"archive_test_stub\").resolve()\n    mocker.patch.object(source.tempfile, \"mkdtemp\", return_value=test_parent)\n    mocker.patch.object(source.utils, \"stream_download\", return_value=test_archive)\n    # Test Remote Stub\n    remote_stub = source.get_source(test_urls[\"download\"])\n    with remote_stub.ready() as source_path:\n        print(list(source_path.parent.iterdir()))\n        assert (source_path / \"info.json\").exists()\n        assert len(list(source_path.iterdir())) == 3\n\n\ndef test_stub_repo_locator(stub_repo):  # noqa\n    locator = source.RepoStubLocator(stub_repo)\n    assert locator.prepare(\"stub1-foo\") == \"https://test-manifest/stub1-foo\"\n"
  },
  {
    "path": "tests/test_stubs/bad_test_stub/modules.json",
    "content": "[\n    {\n        \"nodename\": \"esp32\",\n        \"release\": \"1.10.0\"\n    },\n    {\n        \"pathtofile\": \"/foobar/foo/bar.py\",\n        \"something\": \"bar\"\n    }\n]\n"
  },
  {
    "path": "tests/test_stubs/esp32_test_stub/frozen/ntptime.py",
    "content": "try:\n    import usocket as socket\nexcept:\n    import socket\ntry:\n    import ustruct as struct\nexcept:\n    import struct\n\n# (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60\nNTP_DELTA = 3155673600\n\nhost = \"pool.ntp.org\"\n\n\ndef time():\n    NTP_QUERY = bytearray(48)\n    NTP_QUERY[0] = 0x1B\n    addr = socket.getaddrinfo(host, 123)[0][-1]\n    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n    s.settimeout(1)\n    res = s.sendto(NTP_QUERY, addr)\n    msg = s.recv(48)\n    s.close()\n    val = struct.unpack(\"!I\", msg[40:44])[0]\n    return val - NTP_DELTA\n\n\n# There's currently no timezone support in MicroPython, so\n# utime.localtime() will return UTC time (as if it was .gmtime())\ndef settime():\n    t = time()\n    import machine\n    import utime\n\n    tm = utime.localtime(t)\n    tm = tm[0:3] + (0,) + tm[3:6] + (0,)\n    machine.RTC().datetime(tm)\n    print(utime.localtime())\n"
  },
  {
    "path": "tests/test_stubs/esp32_test_stub/frozen/ntptime.pyi",
    "content": "# make_stub_files: Fri 21 Jun 2019 at 00:44:00\n\nfrom typing import Any, Dict, Optional, Sequence, Tuple, Union\n\nNode = Any\n\ndef time() -> Any: ...\n\n#   0: return val-NTP_DELTA\n# ? 0: return val-NTP_DELTA\ndef settime() -> None: ...\n"
  },
  {
    "path": "tests/test_stubs/esp32_test_stub/info.json",
    "content": "{\n    \"firmware\": {\n        \"machine\": \"ESP32 module with ESP32\",\n        \"firmware\": \"esp32 1.11.0\",\n        \"nodename\": \"esp8266\",\n        \"version\": \"1.11.0\",\n        \"release\": \"1.11.0\",\n        \"sysname\": \"esp32\",\n        \"name\": \"micropython\"\n    },\n    \"stubber\": { \"version\": \"1.2.0\" },\n    \"modules\": [\n        {\n            \"file\": \"/stubs/esp32_1_11_0/umqtt/robust.py\",\n            \"module\": \"umqtt.robust\"\n        },\n        {\n            \"file\": \"/stubs/esp32_1_11_0/umqtt/simple.py\",\n            \"module\": \"umqtt.simple\"\n        }\n    ]\n}\n"
  },
  {
    "path": "tests/test_stubs/esp32_test_stub/stubs/machine.py",
    "content": "\"\"\"\nModule: 'machine' on esp8266 v1.9.4\n\"\"\"\n# MCU: (sysname='esp8266', nodename='esp8266', release='2.2.0-dev(9422289)', version='v1.9.4-8-ga9a3caad0 on 2018-05-11', machine='ESP module with ESP8266')\n# Stubber: 1.1.2\n\n\nclass ADC:\n    \"\"\"\"\"\"\n\n    def read():\n        pass\n\n\nDEEPSLEEP = 4\nDEEPSLEEP_RESET = 5\nHARD_RESET = 6\n\n\nclass I2C:\n    \"\"\"\"\"\"\n"
  },
  {
    "path": "tests/test_stubs/esp32_test_stub/stubs/modules.json",
    "content": "[\n  {\n    \"nodename\": \"esp8266\",\n    \"release\": \"2.2.0-dev(9422289)\",\n    \"version\": \"v1.9.4-8-ga9a3caad0 on 2018-05-11\",\n    \"machine\": \"ESP module with ESP8266\",\n    \"sysname\": \"esp8266\"\n  },\n  { \"stubber\": \"1.1.2\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/uasyncio/core.py\",\n    \"module\": \"uasyncio.core\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/umqtt/robust.py\", \"module\": \"umqtt.robust\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/umqtt/simple.py\", \"module\": \"umqtt.simple\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/urllib/urequest.py\",\n    \"module\": \"urllib.urequest\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/upip.py\", \"module\": \"upip\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/_boot.py\", \"module\": \"_boot\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/_onewire.py\", \"module\": \"_onewire\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/apa102.py\", \"module\": \"apa102\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/array.py\", \"module\": \"array\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/btree.py\", \"module\": \"btree\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/dht.py\", \"module\": \"dht\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ds18x20.py\", \"module\": \"ds18x20\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/errno.py\", \"module\": \"errno\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/esp.py\", \"module\": \"esp\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/example_pub_button.py\",\n    \"module\": \"example_pub_button\"\n  },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/example_sub_led.py\",\n    \"module\": \"example_sub_led\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/flashbdev.py\", \"module\": \"flashbdev\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/framebuf.py\", \"module\": \"framebuf\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/gc.py\", \"module\": \"gc\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/inisetup.py\", \"module\": \"inisetup\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/json.py\", \"module\": \"json\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/math.py\", \"module\": \"math\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/micropython.py\", \"module\": \"micropython\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/neopixel.py\", \"module\": \"neopixel\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/network.py\", \"module\": \"network\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ntptime.py\", \"module\": \"ntptime\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/onewire.py\", \"module\": \"onewire\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/select.py\", \"module\": \"select\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/sys.py\", \"module\": \"sys\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/time.py\", \"module\": \"time\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ubinascii.py\", \"module\": \"ubinascii\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uhashlib.py\", \"module\": \"uhashlib\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uheapq.py\", \"module\": \"uheapq\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ujson.py\", \"module\": \"ujson\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/urandom.py\", \"module\": \"urandom\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ure.py\", \"module\": \"ure\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uselect.py\", \"module\": \"uselect\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ussl.py\", \"module\": \"ussl\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ustruct.py\", \"module\": \"ustruct\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/utime.py\", \"module\": \"utime\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/utimeq.py\", \"module\": \"utimeq\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uzlib.py\", \"module\": \"uzlib\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/websocket_helper.py\",\n    \"module\": \"websocket_helper\"\n  }\n]\n"
  },
  {
    "path": "tests/test_stubs/esp8266_test_stub/frozen/ntptime.py",
    "content": "try:\n    import usocket as socket\nexcept:\n    import socket\ntry:\n    import ustruct as struct\nexcept:\n    import struct\n\n# (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60\nNTP_DELTA = 3155673600\n\nhost = \"pool.ntp.org\"\n\n\ndef time():\n    NTP_QUERY = bytearray(48)\n    NTP_QUERY[0] = 0x1B\n    addr = socket.getaddrinfo(host, 123)[0][-1]\n    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n    s.settimeout(1)\n    res = s.sendto(NTP_QUERY, addr)\n    msg = s.recv(48)\n    s.close()\n    val = struct.unpack(\"!I\", msg[40:44])[0]\n    return val - NTP_DELTA\n\n\n# There's currently no timezone support in MicroPython, so\n# utime.localtime() will return UTC time (as if it was .gmtime())\ndef settime():\n    t = time()\n    import machine\n    import utime\n\n    tm = utime.localtime(t)\n    tm = tm[0:3] + (0,) + tm[3:6] + (0,)\n    machine.RTC().datetime(tm)\n    print(utime.localtime())\n"
  },
  {
    "path": "tests/test_stubs/esp8266_test_stub/frozen/ntptime.pyi",
    "content": "# make_stub_files: Fri 21 Jun 2019 at 00:44:00\n\nfrom typing import Any, Dict, Optional, Sequence, Tuple, Union\n\nNode = Any\n\ndef time() -> Any: ...\n\n#   0: return val-NTP_DELTA\n# ? 0: return val-NTP_DELTA\ndef settime() -> None: ...\n"
  },
  {
    "path": "tests/test_stubs/esp8266_test_stub/info.json",
    "content": "{\n    \"firmware\": {\n        \"machine\": \"ESP module with ESP8266\",\n        \"firmware\": \"esp8266 v1.9.4\",\n        \"nodename\": \"esp8266\",\n        \"version\": \"1.9.4\",\n        \"release\": \"2.2.0-dev(9422289)\",\n        \"sysname\": \"esp8266\",\n        \"name\": \"micropython\"\n    },\n    \"stubber\": { \"version\": \"1.2.0\" },\n    \"modules\": [\n        {\n            \"file\": \"/stubs/esp8266_1_9_4/umqtt/robust.py\",\n            \"module\": \"umqtt.robust\"\n        },\n        {\n            \"file\": \"/stubs/esp8266_1_9_4/umqtt/simple.py\",\n            \"module\": \"umqtt.simple\"\n        }\n    ]\n}\n"
  },
  {
    "path": "tests/test_stubs/esp8266_test_stub/stubs/machine.py",
    "content": "\"\"\"\nModule: 'machine' on esp8266 v1.9.4\n\"\"\"\n# MCU: (sysname='esp8266', nodename='esp8266', release='2.2.0-dev(9422289)', version='v1.9.4-8-ga9a3caad0 on 2018-05-11', machine='ESP module with ESP8266')\n# Stubber: 1.1.2\n\n\nclass ADC:\n    \"\"\"\"\"\"\n\n    def read():\n        pass\n\n\nDEEPSLEEP = 4\nDEEPSLEEP_RESET = 5\nHARD_RESET = 6\n\n\nclass I2C:\n    \"\"\"\"\"\"\n"
  },
  {
    "path": "tests/test_stubs/esp8266_test_stub/stubs/modules.json",
    "content": "[\n  {\n    \"nodename\": \"esp8266\",\n    \"release\": \"2.2.0-dev(9422289)\",\n    \"version\": \"v1.9.4-8-ga9a3caad0 on 2018-05-11\",\n    \"machine\": \"ESP module with ESP8266\",\n    \"sysname\": \"esp8266\"\n  },\n  { \"stubber\": \"1.1.2\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/uasyncio/core.py\",\n    \"module\": \"uasyncio.core\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/umqtt/robust.py\", \"module\": \"umqtt.robust\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/umqtt/simple.py\", \"module\": \"umqtt.simple\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/urllib/urequest.py\",\n    \"module\": \"urllib.urequest\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/upip.py\", \"module\": \"upip\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/_boot.py\", \"module\": \"_boot\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/_onewire.py\", \"module\": \"_onewire\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/apa102.py\", \"module\": \"apa102\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/array.py\", \"module\": \"array\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/btree.py\", \"module\": \"btree\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/dht.py\", \"module\": \"dht\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ds18x20.py\", \"module\": \"ds18x20\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/errno.py\", \"module\": \"errno\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/esp.py\", \"module\": \"esp\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/example_pub_button.py\",\n    \"module\": \"example_pub_button\"\n  },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/example_sub_led.py\",\n    \"module\": \"example_sub_led\"\n  },\n  { \"file\": \"/stubs/esp8266_v1_9_4/flashbdev.py\", \"module\": \"flashbdev\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/framebuf.py\", \"module\": \"framebuf\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/gc.py\", \"module\": \"gc\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/inisetup.py\", \"module\": \"inisetup\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/json.py\", \"module\": \"json\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/math.py\", \"module\": \"math\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/micropython.py\", \"module\": \"micropython\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/neopixel.py\", \"module\": \"neopixel\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/network.py\", \"module\": \"network\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ntptime.py\", \"module\": \"ntptime\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/onewire.py\", \"module\": \"onewire\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/select.py\", \"module\": \"select\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/sys.py\", \"module\": \"sys\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/time.py\", \"module\": \"time\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ubinascii.py\", \"module\": \"ubinascii\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uhashlib.py\", \"module\": \"uhashlib\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uheapq.py\", \"module\": \"uheapq\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ujson.py\", \"module\": \"ujson\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/urandom.py\", \"module\": \"urandom\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ure.py\", \"module\": \"ure\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uselect.py\", \"module\": \"uselect\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ussl.py\", \"module\": \"ussl\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/ustruct.py\", \"module\": \"ustruct\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/utime.py\", \"module\": \"utime\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/utimeq.py\", \"module\": \"utimeq\" },\n  { \"file\": \"/stubs/esp8266_v1_9_4/uzlib.py\", \"module\": \"uzlib\" },\n  {\n    \"file\": \"/stubs/esp8266_v1_9_4/websocket_helper.py\",\n    \"module\": \"websocket_helper\"\n  }\n]\n"
  },
  {
    "path": "tests/test_stubs.py",
    "content": "import shutil\nfrom pathlib import Path\n\nimport pytest\nfrom micropy import exceptions, stubs\n\n\n@pytest.fixture\ndef mock_fware(mocker, shared_datadir):\n    fware_stub = shared_datadir / \"fware_test_stub\"\n    mock_remote = mocker.patch.object(stubs.source.RemoteStubLocator, \"prepare\").return_value\n    mock_remote.__enter__.return_value = fware_stub\n\n\ndef test_stub_validation(shared_datadir):\n    \"\"\"should pass validation\"\"\"\n    stub_path = shared_datadir / \"esp8266_test_stub\"\n    manager = stubs.StubManager()\n    manager.validate(stub_path)\n    assert manager.is_valid(stub_path)\n    assert not manager.is_valid(Path(\"/foobar/bar\"))\n\n\ndef test_bad_stub_validation(shared_datadir, mocker):\n    \"\"\"should fail validation\"\"\"\n    stub_path = shared_datadir / \"esp8266_test_stub\"\n    manager = stubs.StubManager()\n    mock_validate = mocker.patch.object(stubs.stubs.utils, \"Validator\")\n    mock_validate.return_value.validate.side_effect = [Exception, FileNotFoundError]\n    with pytest.raises(exceptions.StubValidationError):\n        manager.validate(stub_path)\n    with pytest.raises(exceptions.StubError):\n        manager.validate(Path(\"/foobar/foo\"))\n\n\ndef test_bad_stub(tmp_path):\n    \"\"\"should raise exception on invalid stub\"\"\"\n    with pytest.raises(FileNotFoundError):\n        stubs.stubs.DeviceStub(tmp_path)\n\n\ndef test_valid_stub(shared_datadir):\n    \"\"\"should have all attributes\"\"\"\n    stub_path = shared_datadir / \"esp8266_test_stub\"\n    stub = stubs.stubs.DeviceStub(stub_path)\n    stub_2 = stubs.stubs.DeviceStub(stub_path)\n    fware = stubs.stubs.FirmwareStub(shared_datadir / \"fware_test_stub\")\n    assert stub == stub_2\n    expect_fware = {\n        \"machine\": \"ESP module with ESP8266\",\n        \"firmware\": \"esp8266 v1.9.4\",\n        \"nodename\": \"esp8266\",\n        \"version\": \"1.9.4\",\n        \"release\": \"2.2.0-dev(9422289)\",\n        \"sysname\": \"esp8266\",\n        \"name\": \"micropython\",\n    }\n    expect_repr = (\n        \"DeviceStub(sysname=esp8266, firmware=micropython,\" f\" version=1.9.4, path={stub_path})\"\n    )\n    assert stub.path.exists()\n    assert stub.stubs.exists()\n    assert stub.frozen.exists()\n    assert stub.version == \"1.9.4\"\n    assert stub.firm_info == expect_fware\n    assert repr(stub) == expect_repr\n    assert str(stub) == \"esp8266-1.9.4\"\n    assert stub.firmware_name == \"micropython\"\n    del stub.firm_info[\"name\"]\n    assert stub.firmware_name == \"esp8266 v1.9.4\"\n    stub.firmware = fware\n    assert stub.firmware_name == \"micropython\"\n    assert str(stub) == \"esp8266-micropython-1.9.4\"\n\n\ndef test_valid_fware_stub(shared_datadir):\n    stub_path = shared_datadir / \"fware_test_stub\"\n    stub = stubs.stubs.FirmwareStub(stub_path)\n    assert str(stub) == \"micropython\"\n    assert stub.frozen.exists()\n    assert repr(stub) == (\"FirmwareStub(firmware=micropython,\" \" repo=micropython/micropython)\")\n\n\ndef test_resolve_stub(shared_datadir):\n    \"\"\"should resolve correct stub type\"\"\"\n    device_stub = shared_datadir / \"esp8266_test_stub\"\n    fware_stub = shared_datadir / \"fware_test_stub\"\n    invalid_stub = shared_datadir / \"esp8266_invalid_stub\"\n    manager = stubs.StubManager()\n    stub_type = manager._get_stubtype(device_stub)\n    assert stub_type == stubs.stubs.DeviceStub\n    stub_type = manager._get_stubtype(fware_stub)\n    assert stub_type == stubs.stubs.FirmwareStub\n    with pytest.raises(exceptions.StubError):\n        manager._get_stubtype(Path(\"/foobar/foo\"))\n    with pytest.raises(exceptions.StubValidationError):\n        manager._get_stubtype(invalid_stub)\n\n\ndef test_resolve_firmware(tmp_path, shared_datadir):\n    \"\"\"should resolve firmware\"\"\"\n    device_stub = shared_datadir / \"esp8266_test_stub\"\n    fware_stub_path = shared_datadir / \"fware_test_stub\"\n    manager = stubs.StubManager(resource=tmp_path)\n    fware_stub = manager.add(fware_stub_path)\n    dev_stub = stubs.stubs.DeviceStub(device_stub)\n    resolved = manager.resolve_firmware(dev_stub)\n    assert fware_stub == resolved\n\n\ndef test_add_single_stub(shared_datadir, tmp_path):\n    \"\"\"should add a single stub\"\"\"\n    stub_path = shared_datadir / \"esp8266_test_stub\"\n    manager = stubs.StubManager()\n    manager.add(stub_path, dest=tmp_path)\n    assert len(manager) == 1\n    assert stub_path.name in [d.name for d in tmp_path.iterdir()]\n\n\ndef test_add_stubs_from_dir(datadir, tmp_path):\n    \"\"\"should add all valid stubs in directory\"\"\"\n    manager = stubs.StubManager()\n    manager.add(datadir, dest=tmp_path)\n    assert len(manager) == 2\n    assert len(list(tmp_path.iterdir())) - 1 == len(manager)\n    assert manager._should_recurse(datadir)\n    with pytest.raises(exceptions.StubError):\n        empty_path = tmp_path / \"empty\"\n        empty_path.mkdir()\n        manager._should_recurse(empty_path)\n\n\ndef test_add_with_resource(datadir, mock_fware, tmp_path, mocker):\n    \"\"\"should not require dest kwarg\"\"\"\n    resource = tmp_path / \"tmp_resource\"\n    resource.mkdir()\n    load_spy = mocker.spy(stubs.StubManager, \"_load\")\n    manager = stubs.StubManager(resource=resource)\n    manager.add(datadir)\n    assert len(manager) == 2\n    assert \"esp8266_test_stub\" in [p.name for p in resource.iterdir()]\n    assert load_spy.call_count == 5\n    # Should not add any new stubs\n    assert manager.add(datadir)\n    assert load_spy.call_count == 5\n    # Should force load\n    assert manager.add(datadir, force=True)\n    assert load_spy.call_count == 10\n\n\ndef test_add_no_resource_no_dest(datadir, mock_fware):\n    \"\"\"should fail with typeerror\"\"\"\n    manager = stubs.StubManager()\n    with pytest.raises(TypeError):\n        manager.add(datadir)\n\n\ndef test_loads_from_resource(datadir, mock_fware):\n    \"\"\"should load from resource if provided\"\"\"\n    manager = stubs.StubManager(resource=datadir)\n    assert len(manager) == 2\n\n\ndef test_name_property(shared_datadir):\n    \"\"\"should raise error if name is not overridden\"\"\"\n    test_stub = shared_datadir / \"esp8266_test_stub\"\n\n    class ErrorStub(stubs.stubs.Stub):\n        def __init__(self, path, copy_to=None, **kwargs):\n            return super().__init__(path, copy_to=copy_to, **kwargs)\n\n    with pytest.raises(NotImplementedError):\n        x = ErrorStub(test_stub)\n        x.name\n\n\ndef test_stub_resolve_link(mock_mp_stubs, tmp_path):\n    \"\"\"should create DeviceStub from symlink\"\"\"\n    stub = list(mock_mp_stubs.stubs)[0]\n    link_path = tmp_path / \"stub_symlink\"\n    linked_stub = stubs.stubs.DeviceStub.resolve_link(stub, link_path)\n    assert stub == linked_stub\n    assert stub.path != linked_stub.path\n    assert linked_stub.path.is_symlink()\n    assert linked_stub.path.resolve() == stub.path\n\n\ndef test_manager_resolve_subresource(mock_mp_stubs, tmp_path):\n    \"\"\"should create StubManager from subresource symlinks\"\"\"\n    test_stubs = list(mock_mp_stubs.stubs)[:2]\n    subresource = tmp_path / \"stub_subresource\"\n    subresource.mkdir()\n    manager = mock_mp_stubs.stubs.resolve_subresource(test_stubs, subresource)\n    linked_stub = list(manager)[0]\n    assert linked_stub.path.is_symlink()\n    assert linked_stub in list(mock_mp_stubs.stubs)\n\n\ndef test_load_firmware_first(mocker, tmp_path, shared_datadir):\n    \"\"\"should always load firmware first\"\"\"\n    mock_manager = mocker.patch.object(stubs.StubManager, \"_load\")\n    mock_iterdir = mocker.patch.object(stubs.stubs.Path, \"iterdir\")\n    # mock_mgr = mock_manager.return_value\n    tmp_path = tmp_path / \"fware_first_test\"\n    tmp_resource = tmp_path / \"tmp_resource\"\n    tmp_resource.mkdir(parents=True)\n    test_stub = shared_datadir / \"esp32_test_stub\"\n    test_fware = shared_datadir / \"fware_test_stub\"\n    # Ensure Firmware loads first, regardless of how Path.iterdir() orders it\n    shutil.copytree(test_stub, (tmp_path / \"99_esp32_test_stub\"))\n    shutil.copytree(test_fware, (tmp_path / \"00_fware_test_stub\"))\n    mock_iterdir.return_value = (test_stub, test_fware)\n    manager = stubs.StubManager(resource=tmp_resource)\n    manager.load_from(tmp_path)\n    # Get First call args\n    fargs, _ = mock_manager.call_args_list[0]\n    assert fargs[0].location == test_fware\n\n\ndef test_iter_by_firm_stubs(mocker):\n    \"\"\"should iter stubs by firmware\"\"\"\n    firm_stub = mocker.MagicMock()\n    dev_stub = mocker.MagicMock()\n    dev_stub.firmware = firm_stub\n    unk_stub = mocker.MagicMock()\n    unk_stub.firmware = None\n    manager = stubs.StubManager()\n    manager._loaded = {firm_stub, dev_stub, unk_stub}\n    manager._firmware = {firm_stub}\n    stub_iter = list(manager.iter_by_firmware())\n    assert stub_iter == [(firm_stub, [dev_stub]), (\"Unknown\", [unk_stub])]\n"
  },
  {
    "path": "tests/test_stubs_repo.py",
    "content": "import pytest\nfrom micropy.stubs import RepositoryInfo, StubPackage, StubRepository, StubsManifest\n\n\nclass ManifestStub(StubsManifest[StubPackage]):\n    def resolve_package_url(self, package: StubPackage) -> str:\n        return f\"https://test-manifest/{package.name}\"\n\n\nTest1Manifest = ManifestStub(\n    repository=RepositoryInfo(\n        name=\"Test\", display_name=\"Test Display\", source=\"https://test-manifest.com\"\n    ),\n    packages=frozenset(\n        [\n            StubPackage(name=\"stub1-foo\", version=\"1.0.0\"),\n            StubPackage(name=\"stub1-foo\", version=\"1.1.0\"),\n            StubPackage(name=\"stub1-foo\", version=\"2.0.0\"),\n            StubPackage(name=\"stub2-bar\", version=\"2.0.0\"),\n        ]\n    ),\n)\n\nTest2Manifest = ManifestStub(\n    repository=RepositoryInfo(\n        name=\"Test2\", display_name=\"Test Display2\", source=\"https://test2-manifest.com\"\n    ),\n    packages=frozenset(\n        [\n            StubPackage(name=\"stub3-thing\", version=\"3.0.0\"),\n            StubPackage(name=\"stub3-thing\", version=\"3.1.0\"),\n            StubPackage(name=\"stub4-device\", version=\"4.0.0\"),\n        ]\n    ),\n)\n\n\n@pytest.fixture\ndef stub_repo():\n    repo = StubRepository(manifests=[Test1Manifest, Test2Manifest])\n    return repo\n\n\ndef test_repo_inits(stub_repo):\n    assert len(stub_repo.manifests) == 2\n    assert len(list(stub_repo.packages)) == 7\n\n\n@pytest.mark.parametrize(\n    \"query,expect_name,include_versions\",\n    [\n        (\"stub1\", [\"Test/stub1-foo-2.0.0\"], False),\n        (\"DEvICE\", [\"Test2/stub4-device-4.0.0\"], False),\n        (\n            \"stub\",\n            [\n                \"Test/stub1-foo-2.0.0\",\n                \"Test/stub2-bar-2.0.0\",\n                \"Test2/stub3-thing-3.1.0\",\n                \"Test2/stub4-device-4.0.0\",\n            ],\n            False,\n        ),\n        (\n            \"stub\",\n            [\n                \"Test/stub1-foo-1.0.0\",\n                \"Test/stub1-foo-1.1.0\",\n                \"Test/stub1-foo-2.0.0\",\n                \"Test/stub2-bar-2.0.0\",\n                \"Test2/stub3-thing-3.0.0\",\n                \"Test2/stub3-thing-3.1.0\",\n                \"Test2/stub4-device-4.0.0\",\n            ],\n            True,\n        ),\n    ],\n)\ndef test_repo_search(stub_repo, query, expect_name, include_versions):\n    results = stub_repo.search(query, include_versions=include_versions)\n    names = [i.absolute_versioned_name for i in results]\n    print(names)\n    assert sorted(names) == sorted(expect_name)\n"
  },
  {
    "path": "tests/test_template.py",
    "content": "import json\nfrom pathlib import Path\n\nimport pylint.lint\nimport pytest\nfrom micropy.project.template import Template, TemplateProvider\n\n\n@pytest.fixture\ndef stub_context(mock_mp_stubs):\n    stubs = list(mock_mp_stubs.stubs)[:3]\n    stub_paths = [stub.stubs for stub in stubs]\n    frozen_paths = [stub.frozen for stub in stubs]\n    fware_paths = [stub.firmware.frozen for stub in stubs]\n    ctx_paths = [*stub_paths, *frozen_paths, *fware_paths]\n    return (stubs, (stub_paths, frozen_paths, fware_paths), ctx_paths)\n\n\ndef test_vscode_template(stub_context, shared_datadir, tmp_path, mock_checks):\n    stubs, paths, ctx_paths = stub_context\n    prov = TemplateProvider([\"vscode\"])\n    ctx_datadir = tmp_path / \"ctx_cata\"\n    ctx_datadir.mkdir(exist_ok=True)\n    # Add test local path\n    ctx_local = ctx_datadir / \"src\" / \"lib\" / \"somelib\"\n    ctx_local.mkdir(parents=True)\n    ctx_absolute = Path(\"/fakedir/notinprojectdir/somelib\")\n    ctx_local_paths = [ctx_local, ctx_absolute]\n    prov.render_to(\n        \"vscode\",\n        tmp_path,\n        stubs=stubs,\n        paths=ctx_paths,\n        datadir=ctx_datadir,\n        local_paths=ctx_local_paths,\n    )\n    expected_path = tmp_path / \".vscode\" / \"settings.json\"\n    out_content = expected_path.read_text()\n    print(out_content)\n    # Get rid of comments\n    with expected_path.open() as f:\n        lines = [line.strip() for line in f.readlines() if line]\n        valid = [line for line in lines if \"//\" not in line[:2]]\n    # Valid JSON?\n    expect_paths = [str(p.relative_to(tmp_path)) for p in ctx_paths]\n    expect_paths.append(str(ctx_local.relative_to(tmp_path)))  # add local path (should be relative)\n    # local path outside of project dir (must be absolute)\n    expect_paths.append(str(ctx_absolute.absolute()))\n    content = json.loads(\"\\n\".join(valid))\n    assert sorted(expect_paths) == sorted(content[\"python.autoComplete.extraPaths\"])\n    assert expected_path.exists()\n    # Test Update\n    ctx_paths.append(tmp_path / \"foobar\" / \"foo.py\")\n    prov.update(\n        \"vscode\",\n        tmp_path,\n        stubs=stubs,\n        paths=ctx_paths,\n        datadir=ctx_datadir,\n        local_paths=ctx_local_paths,\n    )\n    content = json.loads(expected_path.read_text())\n    expect_paths.append(str((tmp_path / \"foobar\" / \"foo.py\").relative_to(tmp_path)))\n    assert sorted(expect_paths) == sorted(content[\"python.autoComplete.extraPaths\"])\n    # Test update with missing file\n    expected_path.unlink()  # delete file\n    prov.update(\"vscode\", tmp_path, stubs=stubs, paths=ctx_paths, datadir=ctx_datadir)\n    assert expected_path.exists()\n\n\ndef test_pylint_template(stub_context, tmp_path):\n    def test_pylint_load():\n        try:\n            lint_args = [\"--rcfile\", str(expected_path.absolute())]\n            pylint.lint.Run(lint_args)\n        except SyntaxError:\n            pytest.fail(str(SyntaxError))\n        except:  # noqa\n            pass\n\n    stubs, paths, ctx_paths = stub_context\n    ctx_datadir = tmp_path / \"ctx_cata\"\n    ctx_datadir.mkdir(exist_ok=True)\n    prov = TemplateProvider([\"pylint\"])\n    prov.render_to(\"pylint\", tmp_path, stubs=stubs, paths=ctx_paths, datadir=ctx_datadir)\n    expected_path = tmp_path / \".pylintrc\"\n    assert expected_path.exists()\n    # Will Pylint load it?\n    test_pylint_load()\n    # Test Update\n    new_path = tmp_path / \".micropy\" / \"foobar\" / \"foo\"\n    ctx_paths.append(new_path)\n    prov.update(\"pylint\", tmp_path, stubs=stubs, paths=ctx_paths, datadir=ctx_datadir)\n    init_hook = expected_path.read_text().splitlines(True)[2]\n    hook_imports = init_hook.split(\",\")\n    hook_path = str(Path(\".micropy/foobar/foo\")).replace(\n        \"\\\\\", \"/\"\n    )  # no need to use \\\\ on pylint Windows\n    assert f' \"{hook_path}\"' in hook_imports\n    test_pylint_load()\n\n\ndef test_generic_template(mock_mp_stubs, tmp_path):\n    prov = TemplateProvider([\"bootstrap\", \"pymakr\"])\n    prov.render_to(\"boot\", tmp_path)\n    expected_path = tmp_path / \"src\" / \"boot.py\"\n    assert expected_path.exists()\n    expected_content = (prov.TEMPLATE_DIR / \"src\" / \"boot.py\").read_text()\n    out_content = expected_path.read_text()\n    print(out_content)\n    assert expected_content.strip() == out_content.strip()\n    templ = prov.get(\"boot\")\n    assert templ.update(tmp_path) is None\n\n\ndef test_no_context():\n    class BadTemplate(Template):\n        def __init__(self, template, **kwargs):\n            return super().__init__(template, **kwargs)\n\n    with pytest.raises(NotImplementedError):\n        x = BadTemplate(\"abc\")\n        print(x.context)\n"
  },
  {
    "path": "tests/test_utils/fail.json",
    "content": "[\n    {\n        \"nodename\": \"esp32\",\n        \"release\": \"1.10.0\"\n    },\n    {\n        \"pathtofile\": \"/foobar/foo/bar.py\",\n        \"something\": \"bar\"\n    }\n]\n"
  },
  {
    "path": "tests/test_utils/pass.json",
    "content": "[\n    {\n        \"nodename\": \"esp32\",\n        \"release\": \"1.10.0\",\n        \"version\": \"'v1.10-247-g0fb15fc3f on 2019-03-29\",\n        \"machine\": \"ESP32 module with ESP32\",\n        \"sysname\": \"esp32\"\n    },\n    {\n        \"stubber\": \"1.1.2\"\n    },\n    {\n        \"file\": \"/foobar/foo/bar.py\",\n        \"module\": \"bar\"\n    },\n    {\n        \"file\": \"/barfoo/bar/foo.py\",\n        \"module\": \"foo\"\n    }\n]\n"
  },
  {
    "path": "tests/test_utils/schema.json",
    "content": "{\n    \"type\": \"array\",\n    \"items\": {\n        \"oneOf\": [\n            {\n                \"type\": \"object\",\n                \"required\": [\n                    \"machine\",\n                    \"nodename\",\n                    \"release\",\n                    \"sysname\",\n                    \"version\"\n                ],\n                \"properties\": {\n                    \"machine\": { \"type\": \"string\" },\n                    \"nodename\": { \"type\": \"string\" },\n                    \"release\": { \"type\": \"string\" },\n                    \"sysname\": { \"type\": \"string\" },\n                    \"version\": { \"type\": \"string\" }\n                }\n            },\n            {\n                \"type\": \"object\",\n                \"required\": [\"stubber\"],\n                \"properties\": { \"stubber\": { \"type\": \"string\" } }\n            },\n            {\n                \"type\": \"object\",\n                \"required\": [\"file\", \"module\"],\n                \"properties\": {\n                    \"file\": { \"type\": \"string\" },\n                    \"module\": { \"type\": \"string\" }\n                }\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import io\nimport sys\n\nimport pytest\nimport requests\nfrom jsonschema import ValidationError\nfrom micropy import utils\nfrom requests.exceptions import ConnectionError, HTTPError, InvalidURL\n\n\n@pytest.fixture\ndef schema(datadir):\n    file = datadir / \"schema.json\"\n    pass_file = datadir / \"pass.json\"\n    fail_file = datadir / \"fail.json\"\n    return (file, pass_file, fail_file)\n\n\ndef test_validate(schema):\n    \"\"\"Test for successful validation\"\"\"\n    schema, pass_file, _ = schema\n    val = utils.Validator(schema_path=schema)\n    val.validate(pass_file)\n\n\ndef test_fail_validate(schema):\n    \"\"\"Test for invalid file\"\"\"\n    schema, _, fail_file = schema\n    val = utils.Validator(schema_path=schema)\n    with pytest.raises(ValidationError):\n        val.validate(fail_file)\n\n\ndef test_is_url(test_urls):\n    \"\"\"should respond true/false for url\"\"\"\n    u = test_urls\n    assert utils.is_url(u[\"valid\"])\n    assert utils.is_url(u[\"valid_https\"])\n    assert not utils.is_url(u[\"invalid\"])\n    assert not utils.is_url(u[\"invalid_file\"])\n\n\ndef test_ensure_valid_url(mocker, test_urls):\n    \"\"\"should ensure url is valid\"\"\"\n    u = test_urls\n    with pytest.raises(InvalidURL):\n        utils.ensure_valid_url(test_urls[\"invalid\"])\n    with pytest.raises(ConnectionError):\n        mocker.patch.object(utils, \"is_url\", return_value=True)\n        mock_head = mocker.patch.object(requests, \"head\")\n        mock_head.side_effect = [ConnectionError]\n        utils.ensure_valid_url(u[\"valid\"])\n    mocker.stopall()\n    with pytest.raises(HTTPError):\n        utils.ensure_valid_url(u[\"bad_resp\"])\n    result = utils.ensure_valid_url(u[\"valid\"])\n    assert result == u[\"valid\"]\n\n\ndef test_ensure_existing_dir(tmp_path):\n    \"\"\"should ensure dir exists\"\"\"\n    not_exist = tmp_path / \"i_dont_exist\"\n    file = tmp_path / \"file.txt\"\n    file.touch()\n    with pytest.raises(NotADirectoryError):\n        utils.ensure_existing_dir(not_exist)\n    with pytest.raises(NotADirectoryError):\n        utils.ensure_existing_dir(file)\n    result = utils.ensure_existing_dir(str(tmp_path))\n    assert result == tmp_path\n    assert result.exists()\n    assert result.is_dir()\n\n\ndef test_is_downloadable(mocker, test_urls):\n    \"\"\"should check if url can be downloaded from\"\"\"\n    u = test_urls\n    uheaders = u[\"headers\"]\n    mock_head = mocker.patch.object(requests, \"head\")\n    head_mock_val = mocker.PropertyMock(\n        side_effect=[uheaders[\"not_download\"], uheaders[\"can_download\"]]\n    )\n    type(mock_head.return_value).headers = head_mock_val\n    assert not utils.is_downloadable(u[\"valid\"])\n    assert not utils.is_downloadable(\"not-a-real-url\")\n    assert utils.is_downloadable(u[\"valid\"])\n\n\ndef test_get_url_filename(test_urls):\n    \"\"\"should return filename\"\"\"\n    filename = \"archive_test_stub.tar.gz\"\n    result = utils.get_url_filename(test_urls[\"download\"])\n    assert result == filename\n\n\ndef test_is_existing_dir(tmp_path):\n    bad_path = tmp_path / \"not-real-path\"\n    is_file = tmp_path / \"file.txt\"\n    is_file.touch()\n    assert not utils.is_existing_dir(bad_path)\n    assert not utils.is_existing_dir(is_file)\n    assert utils.is_existing_dir(tmp_path)\n\n\ndef test_search_xml(mocker, shared_datadir, test_urls):\n    u = test_urls\n    test_xml = shared_datadir / \"test_source.xml\"\n    mock_get = mocker.patch.object(requests, \"get\")\n    with test_xml.open(\"rb\") as f:\n        type(mock_get.return_value).content = f.read()\n    results = utils.search_xml(u[\"valid\"], \"Key\", ignore_cache=True)\n    assert sorted(results) == sorted(\n        [\"packages/esp32-micropython-1.10.0.tar.gz\", \"packages/esp32-micropython-1.11.0.tar.gz\"]\n    )\n\n\n@pytest.mark.xfail(sys.version_info >= (3, 8), reason=\"requires python >= 3.8\")\ndef test_generate_stub__py37(tmp_path):\n    with pytest.raises(ImportError):\n        expect_path = tmp_path / \"foo.py\"\n        expect_path.touch()\n        utils.generate_stub(expect_path)\n\n\n@pytest.mark.xfail(sys.version_info >= (3, 8), reason=\"requires python >= 3.8\")\ndef test_prepare_create_stubs__py37():\n    with pytest.raises(ImportError):\n        utils.stub.prepare_create_stubs()\n\n\n@pytest.mark.xfail(sys.version_info < (3, 8), reason=\"requires python >= 3.8\", raises=ImportError)\ndef test_prepare_create_stubs():\n    create_stubs = utils.stub.prepare_create_stubs()\n    assert isinstance(create_stubs, io.StringIO)\n    assert len(create_stubs.getvalue()) > 1\n\n\ndef test_generate_stub(shared_datadir, tmp_path, mocker):\n    mock_stubber = mocker.patch.object(utils.stub, \"stubmaker\")\n    expect_path = tmp_path / \"foo.py\"\n    expect_path.touch()\n    result = utils.generate_stub(expect_path)\n    mock_stubber.generate_pyi_from_file.assert_called_once()\n    assert result == (expect_path, expect_path.with_suffix(\".pyi\"))\n    # Test print monkeypatch\n    print_mock = mocker.Mock(return_value=None)\n    utils.generate_stub(expect_path, log_func=print_mock)\n\n\ndef test_get_package_meta(mocker, requests_mock):\n    \"\"\"should get package meta\"\"\"\n    mock_data = {\n        \"releases\": {\n            \"0.0.0\": [{\"url\": \"early-version.tar.gz\"}],\n            \"0.1.0\": [\n                {\n                    \"url\": \"do-not-return-me\",\n                },\n                {\"url\": \"return-me.tar.gz\"},\n            ],\n        }\n    }\n    requests_mock.get(\"https://pypi.org/pypi/foobar/json\", json=mock_data)\n    result = utils.get_package_meta(\"foobar\", \"https://pypi.org/pypi/foobar/json\")\n    assert result == {\"url\": \"return-me.tar.gz\"}\n    result = utils.get_package_meta(\"foobar==0.0.0\", \"https://pypi.org/pypi/foobar/json\")\n    assert result == {\"url\": \"early-version.tar.gz\"}\n\n\ndef test_extract_tarbytes(mocker):\n    \"\"\"should extract tar file from memory\"\"\"\n    test_bytes = bytearray(\"foobar\", \"utf-8\")\n    mock_io = mocker.patch.object(utils.helpers.io, \"BytesIO\")\n    mock_io.return_value = io.BytesIO(test_bytes)\n    mock_tarfile = mocker.patch.object(utils.helpers, \"tarfile\")\n    mock_tar = mock_tarfile.open.return_value.__enter__.return_value\n    utils.extract_tarbytes(test_bytes, \"foobar\")\n    mock_tarfile.open.assert_called_once_with(fileobj=io.BytesIO(test_bytes), mode=\"r:gz\")\n    mock_tar.extractall.assert_called_once_with(\"foobar\", mocker.ANY, numeric_owner=mocker.ANY)\n\n\ndef test_iter_requirements(mocker, tmp_path):\n    \"\"\"should iter requirements\"\"\"\n    tmp_file = tmp_path / \"tmp_reqs.txt\"\n    tmp_file.touch()\n    tmp_file.write_text(\"micropy-cli==1.0.0\")\n    result = next(utils.iter_requirements(tmp_file))\n    assert result.name == \"micropy-cli\"\n    assert result.specs == [(\"==\", \"1.0.0\")]\n\n\ndef test_create_dir_link(mocker, tmp_path):\n    \"\"\"Should create a symlink or directory junction if needed\"\"\"\n    targ_path = tmp_path / \"target_dir\"\n    targ_path.mkdir()\n    link_path = tmp_path / \"link_path\"\n    mock_sys = mocker.patch.object(utils.helpers, \"sys\")\n    mock_platform = type(mock_sys).platform = mocker.PropertyMock()\n    mock_subproc = mocker.patch.object(utils.helpers, \"subproc\")\n    mock_path = mocker.patch.object(utils.helpers, \"Path\").return_value\n    mock_path.symlink_to.side_effect = [mocker.ANY, OSError, OSError, OSError]\n\n    mock_platform.return_value = \"linux\"\n    # Test POSIX (should not raise exception)\n    utils.create_dir_link(link_path, targ_path)\n    mock_path.symlink_to.assert_called_once()\n    assert mock_subproc.call_count == 0\n    # Test POSIX failed for unknown reason\n    with pytest.raises(OSError):\n        utils.create_dir_link(link_path, targ_path)\n    # Test Windows (should try to make symlink, fallback on DJ)\n    mock_platform.return_value = \"win32\"\n    mock_subproc.call.return_value = 0\n    utils.create_dir_link(link_path, targ_path)\n    assert mock_subproc.call.call_count == 1\n    # Test Windows fails for some reason\n    mock_subproc.call.return_value = 1\n    with pytest.raises(OSError):\n        utils.create_dir_link(link_path, targ_path)\n\n\ndef test_is_dir_link(mocker, tmp_path):\n    \"\"\"Should test if a path is a symlink or directory junction\"\"\"\n    link_path = tmp_path / \"link\"\n    targ_path = tmp_path / \"target\"\n    mock_sys = mocker.patch.object(utils.helpers, \"sys\")\n    mock_platform = type(mock_sys).platform = mocker.PropertyMock()\n    mock_path = mocker.patch.object(utils.helpers, \"Path\").return_value\n    mock_path.is_symlink.side_effect = [True, False, False, False]\n    # Test Symlink (POSIX)\n    mock_platform.return_value = \"linux\"\n    assert utils.is_dir_link(link_path)\n    assert not utils.is_dir_link(link_path)\n    # Test Directory Junction (Windows)\n    mock_platform.return_value = \"win32\"\n    # From what I can tell, while Path.is_symlink always returns false for DJs.\n    # However, on a DJ, Path.absolute will return the absolute path to the DJ,\n    # while Path.resolve will return the absolute path to the source directory.\n    # With this in mind, this check SHOULD work.\n    mock_path.resolve.return_value = targ_path\n    mock_path.absolute.return_value = link_path\n    assert utils.is_dir_link(link_path)\n    mock_path.absolute.return_value = targ_path\n    assert not utils.is_dir_link(link_path)\n\n\n@pytest.mark.parametrize(\n    \"versions,expect\",\n    [\n        ([\"1.0.0rc.1\"], False),\n        ([\"1.0.0rc.1\", \"1.0.0\"], \"1.0.0\"),\n        ([\"1.0.0rc.1\", \"1.0.0\", \"2.0.0rc.1\", \"2.0.0\"], \"2.0.0\"),\n    ],\n)\ndef test_is_update_available(mocker, requests_mock, versions, expect):\n    \"\"\"Test self-update check method\"\"\"\n    fake_data = {\"releases\": {k: [] for k in versions}}\n    requests_mock.get(\"https://pypi.org/pypi/micropy-cli/json\", json=fake_data)\n    mocker.patch(\"micropy.utils._compat.metadata.version\", return_value=\"0.0.0\")\n    utils.helpers.get_cached_data.clear_cache()\n    assert utils.helpers.is_update_available() == expect\n\n\ndef test_stream_download(mocker):\n    \"\"\"Test stream download\"\"\"\n    mock_req = mocker.patch.object(utils.helpers, \"requests\")\n    mock_stream = mocker.MagicMock()\n    mock_stream.headers = {\"content-length\": \"1000\"}\n    mock_req.get.return_value = mock_stream\n    tqdm_mock = mocker.patch.object(utils.helpers, \"tqdm\")\n    utils.stream_download(\"https://someurl.com/file.ext\")\n    expect_args = {\n        \"unit_scale\": True,\n        \"unit_divisor\": 1024,\n        \"smoothing\": 0.1,\n        \"bar_format\": mocker.ANY,\n    }\n    tqdm_mock.assert_called_once_with(total=1000, unit=\"B\", **expect_args)\n"
  }
]