pax_global_header00006660000000000000000000000064144104156210014510gustar00rootroot0000000000000052 comment=539fd4bea6b537365e9588e1182aff27ced8a0da qtsass-0.4.0/000077500000000000000000000000001441041562100130275ustar00rootroot00000000000000qtsass-0.4.0/.authors.yml000066400000000000000000000025431441041562100153210ustar00rootroot00000000000000- name: Andrey Galkin email: andrey@futoin.eu alternate_emails: - andrey@futoin.org num_commits: 5 first_commit: 2019-02-25 16:41:43 github: tmpfork - name: Eric Werner email: ewerybody+github@gmail.com num_commits: 16 first_commit: 2019-08-29 09:56:15 github: ewerybody - name: Matthew Joyce email: matsjoyce@gmail.com num_commits: 2 first_commit: 2019-07-25 07:53:58 github: matsjoyce - name: Dan Bradham email: danielbradham@gmail.com num_commits: 66 first_commit: 2018-05-01 16:28:41 github: danbradham - name: Gonzalo Peña-Castellanos email: goanpeca@gmail.com aliases: - Gonzalo Pena-Castellanos - goanpeca num_commits: 27 first_commit: 2017-11-22 18:15:12 github: goanpeca - name: Carlos Cordoba email: ccordoba12@gmail.com num_commits: 3 first_commit: 2019-04-26 04:36:01 github: ccordoba12 - name: C.A.M. Gerlach email: widenetservices@gmail.com num_commits: 3 first_commit: 2018-06-11 16:08:54 github: CAM-Gerlach - name: Yann Lanthony email: yann.lanthony@gmail.com aliases: - yann.lanthony num_commits: 22 first_commit: 2015-08-16 05:22:05 github: yann-lty - name: Daniel Althviz Moré email: d.althviz10@uniandes.edu.co aliases: - dalthviz - Daniel Althviz github: dalthviz - name: Sebastian Weigand email: s.weigand.phy@gmail.com aliases: - s-weigand github: s-weigandqtsass-0.4.0/.github/000077500000000000000000000000001441041562100143675ustar00rootroot00000000000000qtsass-0.4.0/.github/FUNDING.yml000066400000000000000000000000301441041562100161750ustar00rootroot00000000000000open_collective: spyder qtsass-0.4.0/.github/workflows/000077500000000000000000000000001441041562100164245ustar00rootroot00000000000000qtsass-0.4.0/.github/workflows/ci.yml000066400000000000000000000136701441041562100175510ustar00rootroot00000000000000name: Tests on: # This avoids having duplicate builds for a pull request push: tags: - v** branches: - master pull_request: branches: - master jobs: smoke: name: Linux smoke test Py${{ matrix.PYTHON_VERSION }} runs-on: ubuntu-latest env: CI: True PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} QT_DEBUG_PLUGINS: 1 strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.7', '3.10'] steps: - name: Checkout branch uses: actions/checkout@v2 - name: Install Conda uses: conda-incubator/setup-miniconda@v2 with: activate-environment: test python-version: ${{ matrix.PYTHON_VERSION }} - name: Install Dependencies shell: bash -l {0} run: pip install -r requirements/install.txt - name: Install Test Dependencies shell: bash -l {0} run: pip install -r requirements/dev.txt - name: Install Package shell: bash -l {0} run: python setup.py develop - name: Show Environment shell: bash -l {0} run: | conda info conda list - name: Format if: matrix.PYTHON_VERSION == '3.7' shell: bash -l {0} run: python run_checks_and_format.py - name: Run tests shell: bash -l {0} run: xvfb-run --auto-servernum pytest tests --cov=qtsass --cov-report=term-missing -x -vv - name: Upload coverage to Codecov shell: bash -l {0} run: codecov -t 74b56e56-6c81-4b43-b830-c46638da84a7 linux: name: Linux Py${{ matrix.PYTHON_VERSION }} needs: smoke runs-on: ubuntu-latest env: CI: True PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} QT_DEBUG_PLUGINS: 1 strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.8', '3.9'] steps: - name: Checkout branch uses: actions/checkout@v2 - name: Install Conda uses: conda-incubator/setup-miniconda@v2 with: activate-environment: test python-version: ${{ matrix.PYTHON_VERSION }} - name: Install Dependencies shell: bash -l {0} run: pip install -r requirements/install.txt - name: Install Test Dependencies shell: bash -l {0} run: pip install -r requirements/dev.txt - name: Install Package shell: bash -l {0} run: python setup.py develop - name: Show Environment shell: bash -l {0} run: | conda info conda list - name: Run tests shell: bash -l {0} run: xvfb-run --auto-servernum pytest tests --cov=qtsass --cov-report=term-missing -x -vv - name: Upload coverage to Codecov shell: bash -l {0} run: codecov -t 74b56e56-6c81-4b43-b830-c46638da84a7 macos: name: Mac Py${{ matrix.PYTHON_VERSION }} needs: smoke runs-on: macos-latest env: CI: True PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} QT_DEBUG_PLUGINS: 1 strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.7', '3.8', '3.9', '3.10'] steps: - name: Checkout branch uses: actions/checkout@v2 - name: Install Conda uses: conda-incubator/setup-miniconda@v2 with: activate-environment: test python-version: ${{ matrix.PYTHON_VERSION }} - name: Install Dependencies shell: bash -l {0} run: pip install -r requirements/install.txt - name: Install Test Dependencies shell: bash -l {0} run: pip install -r requirements/dev.txt - name: Install Package shell: bash -l {0} run: python setup.py develop - name: Show Environment shell: bash -l {0} run: | conda info conda list - name: Run tests shell: bash -l {0} run: pytest tests --cov=qtsass --cov-report=term-missing -x -vv - name: Upload coverage to Codecov shell: bash -l {0} run: codecov -t 74b56e56-6c81-4b43-b830-c46638da84a7 windows: name: Windows Py${{ matrix.PYTHON_VERSION }} needs: smoke runs-on: windows-latest env: CI: True PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} QT_DEBUG_PLUGINS: 1 strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.7', '3.8', '3.9', '3.10'] steps: - name: Checkout branch uses: actions/checkout@v2 - name: Install Conda uses: conda-incubator/setup-miniconda@v2 with: activate-environment: test python-version: ${{ matrix.PYTHON_VERSION }} - name: Install Dependencies shell: bash -l {0} run: pip install -r requirements/install.txt - name: Install Test Dependencies shell: bash -l {0} run: pip install -r requirements/dev.txt - name: Install Package shell: bash -l {0} run: python setup.py develop - name: Show Environment shell: bash -l {0} run: | conda info conda list - name: Run tests shell: bash -l {0} run: pytest tests --cov=qtsass --cov-report=term-missing -x -vv - name: Upload coverage to Codecov shell: bash -l {0} run: codecov -t 74b56e56-6c81-4b43-b830-c46638da84a7 deploy: runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') needs: [windows, macos, linux] steps: - uses: actions/checkout@v3 - name: Set up Python 3.8 uses: actions/setup-python@v4 with: python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip wheel setuptools - name: Build dist run: | python setup.py sdist bdist_wheel - name: Publish package uses: pypa/gh-action-pypi-publish@v1.5.1 with: user: __token__ password: ${{ secrets.pypi_password }} qtsass-0.4.0/.gitignore000066400000000000000000000070601441041562100150220ustar00rootroot00000000000000################# ## Eclipse ################# *.orig *.pydevproject .project .metadata bin/ tmp/ *.tmp *.bak *.swp *~.nib local.properties .classpath .settings/ .loadpath # External tool builders .externalToolBuilders/ # Locally stored "Eclipse launch configurations" *.launch # CDT-specific .cproject # PDT-specific .buildpath ################# ## Visual Studio ################# ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.sln.docstates # Build results [Dd]ebug/ [Rr]elease/ x64/ build/ [Bb]in/ [Oo]bj/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* *_i.c *_p.c *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.log *.scc # Visual C++ cache files ipch/ *.aps *.ncb *.opensdf *.sdf *.cachefile # Visual Studio profiler *.psess *.vsp *.vspx # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch *.ncrunch* .*crunch*.local.xml # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.Publish.xml *.pubxml *.publishproj # NuGet Packages Directory ## TODO: If you have NuGet Package Restore enabled, uncomment the next line #packages/ # Windows Azure Build Output csx *.build.csdef # Windows Store app package directory AppPackages/ # Others sql/ *.Cache ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.[Pp]ublish.xml *.pfx *.publishsettings # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file to a newer # Visual Studio version. Backup files are not needed, because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files App_Data/*.mdf App_Data/*.ldf ############# ## Windows detritus ############# # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Mac crap .DS_Store ############# ## Python ############# *.py[cod] # Packages *.egg *.egg-info dist/ build/ eggs/ parts/ var/ sdist/ develop-eggs/ .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox #Translations *.mo #Mr Developer .mr.developer.cfg # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio *.iml ## Directory-based project format: .idea/ # if you remove the above rule, at least ignore the following: # User-specific stuff: # .idea/workspace.xml # .idea/tasks.xml # .idea/dictionaries # Sensitive or high-churn files: # .idea/dataSources.ids # .idea/dataSources.xml # .idea/sqlDataSources.xml # .idea/dynamic.xml # .idea/uiDesigner.xml # Gradle: # .idea/gradle.xml # .idea/libraries # Mongo Explorer plugin: # .idea/mongoSettings.xml ## File-based project format: *.ipr *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties # pytest .pytest_cache # Project virtualenv venv/ # Rever rever/ activate.xsh # Loghub CHANGELOG.temp qtsass-0.4.0/.mailmap000066400000000000000000000017041441041562100144520ustar00rootroot00000000000000# This file was autogenerated by rever: https://regro.github.io/rever-docs/ # This prevent git from showing duplicates with various logging commands. # See the git documentation for more details. The syntax is: # # good-name bad-name # # You can skip bad-name if it is the same as good-name and is unique in the repo. # # This file is up-to-date if the command git log --format="%aN <%aE>" | sort -u # gives no duplicates. Andrey Galkin Andrey Galkin C.A.M. Gerlach Carlos Cordoba Dan Bradham Eric Werner Gonzalo Peña-Castellanos Gonzalo Pena-Castellanos Gonzalo Peña-Castellanos goanpeca Matthew Joyce Yann Lanthony yann.lanthony qtsass-0.4.0/AUTHORS.md000066400000000000000000000011411441041562100144730ustar00rootroot00000000000000# Authors The qtsass project has some great contributors! They are: - [Andrey Galkin](https://github.com/tmpfork) - [C.A.M. Gerlach](https://github.com/CAM-Gerlach) - [Carlos Cordoba](https://github.com/ccordoba12) - [Dan Bradham](https://github.com/danbradham) - [Eric Werner](https://github.com/ewerybody) - [Gonzalo Peña-Castellanos](https://github.com/goanpeca) - [Matthew Joyce](https://github.com/matsjoyce) - [Yann Lanthony](https://github.com/yann-lty) These have been sorted alphabetically. The full list of contributors can be found at: https://github.com/spyder-ide/qtsass/graphs/contributors qtsass-0.4.0/CHANGELOG.md000066400000000000000000000214331441041562100146430ustar00rootroot00000000000000# History of changes ## Version 0.4.0 (2023/03/27) ### Issues Closed * [Issue 73](https://github.com/spyder-ide/qtsass/issues/73) - Release QtSASS 0.4.0 * [Issue 67](https://github.com/spyder-ide/qtsass/issues/67) - Drop support for Python 2.7, 3.5, and 3.6 ([PR 71](https://github.com/spyder-ide/qtsass/pull/71) by [@dalthviz](https://github.com/dalthviz)) In this release 2 issues were closed. ### Pull Requests Merged * [PR 71](https://github.com/spyder-ide/qtsass/pull/71) - PR: Drop support for Python <=3.6, by [@dalthviz](https://github.com/dalthviz) ([67](https://github.com/spyder-ide/qtsass/issues/67)) * [PR 70](https://github.com/spyder-ide/qtsass/pull/70) - PR: Made `compile_filename` file output optional, by [@gentlegiantJGC](https://github.com/gentlegiantJGC) * [PR 69](https://github.com/spyder-ide/qtsass/pull/69) - PR: Update RELEASE.md, by [@dalthviz](https://github.com/dalthviz) * [PR 59](https://github.com/spyder-ide/qtsass/pull/59) - PR: Add the support for QRadialGradient, by [@regrainb](https://github.com/regrainb) ([57](https://github.com/spyder-ide/qtsass/issues/57)) In this release 4 pull requests were closed. ## Version 0.3.2 (2022/09/16) ### Pull Requests Merged * [PR 68](https://github.com/spyder-ide/qtsass/pull/68) - PR: Constraint libsass to 0.21.0 and update .authors.yml file, by [@dalthviz](https://github.com/dalthviz) * [PR 66](https://github.com/spyder-ide/qtsass/pull/66) - PR: Update changelog and authors, by [@dalthviz](https://github.com/dalthviz) * [PR 65](https://github.com/spyder-ide/qtsass/pull/65) - PR:🚇 Add deployment pipeline step, by [@s-weigand](https://github.com/s-weigand) In this release 3 pull requests were closed. ## Version 0.3.1 (2022/09/05) ### Issues Closed * [Issue 60](https://github.com/spyder-ide/qtsass/issues/60) - Release QtSASS 0.3.1 ([PR 64](https://github.com/spyder-ide/qtsass/pull/64) by [@dalthviz](https://github.com/dalthviz)) * [Issue 52](https://github.com/spyder-ide/qtsass/issues/52) - Update 'collections' imports for Python 3.3+ ([PR 54](https://github.com/spyder-ide/qtsass/pull/54) by [@goanpeca](https://github.com/goanpeca)) In this release 2 issues were closed. ### Pull Requests Merged * [PR 64](https://github.com/spyder-ide/qtsass/pull/64) - PR: Update CI Python versions and classifiers, by [@dalthviz](https://github.com/dalthviz) ([60](https://github.com/spyder-ide/qtsass/issues/60)) * [PR 54](https://github.com/spyder-ide/qtsass/pull/54) - PR: Add check for deprecated api between 2 and 3 versions, by [@goanpeca](https://github.com/goanpeca) ([52](https://github.com/spyder-ide/qtsass/issues/52)) In this release 2 pull requests were closed. ## Version qtsass v0.3.0 (2020/03/18) ### Issues Closed * [Issue 50](https://github.com/spyder-ide/qtsass/issues/50) - Add rever to release process ([PR 51](https://github.com/spyder-ide/qtsass/pull/51) by [@goanpeca](https://github.com/goanpeca)) * [Issue 48](https://github.com/spyder-ide/qtsass/issues/48) - Move CI to github actions ([PR 49](https://github.com/spyder-ide/qtsass/pull/49) by [@goanpeca](https://github.com/goanpeca)) In this release 2 issues were closed. ### Pull Requests Merged * [PR 51](https://github.com/spyder-ide/qtsass/pull/51) - PR: Add rever to release process, by [@goanpeca](https://github.com/goanpeca) ([50](https://github.com/spyder-ide/qtsass/issues/50)) * [PR 49](https://github.com/spyder-ide/qtsass/pull/49) - PR: Move to github actions, by [@goanpeca](https://github.com/goanpeca) ([48](https://github.com/spyder-ide/qtsass/issues/48)) In this release 2 pull requests were closed. ## Version 0.2 (2020-02-23) ### Issues Closed * [Issue 43](https://github.com/spyder-ide/qtsass/issues/43) - rgba function breaks on incoming 8bit ints ([PR 40](https://github.com/spyder-ide/qtsass/pull/40) by [@ewerybody](https://github.com/ewerybody)) * [Issue 42](https://github.com/spyder-ide/qtsass/issues/42) - qlineargradient x1,y1,x2,y2 values can be floats! ([PR 40](https://github.com/spyder-ide/qtsass/pull/40) by [@ewerybody](https://github.com/ewerybody)) * [Issue 41](https://github.com/spyder-ide/qtsass/issues/41) - make qtsass watchdog dependence optional In this release 3 issues were closed. ### Pull Requests Merged * [PR 44](https://github.com/spyder-ide/qtsass/pull/44) - Add Watcher api, by [@danbradham](https://github.com/danbradham) * [PR 40](https://github.com/spyder-ide/qtsass/pull/40) - added support for incomplete coords and incoming %-rgba values ..., by [@ewerybody](https://github.com/ewerybody) ([43](https://github.com/spyder-ide/qtsass/issues/43), [42](https://github.com/spyder-ide/qtsass/issues/42)) In this release 2 pull requests were closed. ## Version 0.1.0 (2019-05-05) ### Issues Closed * [Issue 21](https://github.com/spyder-ide/qtsass/issues/21) - Reorganize qtsass package * [Issue 18](https://github.com/spyder-ide/qtsass/issues/18) - CLI - build/watch directory ([PR 23](https://github.com/spyder-ide/qtsass/pull/23) by [@danbradham](https://github.com/danbradham)) * [Issue 17](https://github.com/spyder-ide/qtsass/issues/17) - Create PyPI package ([PR 32](https://github.com/spyder-ide/qtsass/pull/32) by [@goanpeca](https://github.com/goanpeca)) * [Issue 16](https://github.com/spyder-ide/qtsass/issues/16) - Add Windows CI - Appveyor ([PR 19](https://github.com/spyder-ide/qtsass/pull/19) by [@danbradham](https://github.com/danbradham)) * [Issue 12](https://github.com/spyder-ide/qtsass/issues/12) - Move test to a tests folder ([PR 13](https://github.com/spyder-ide/qtsass/pull/13) by [@danbradham](https://github.com/danbradham)) * [Issue 11](https://github.com/spyder-ide/qtsass/issues/11) - Add badges for coverage and travis CI builds ([PR 14](https://github.com/spyder-ide/qtsass/pull/14) by [@danbradham](https://github.com/danbradham)) * [Issue 10](https://github.com/spyder-ide/qtsass/issues/10) - Add coverage to the CI tests with codecov.io ([PR 15](https://github.com/spyder-ide/qtsass/pull/15) by [@goanpeca](https://github.com/goanpeca)) * [Issue 9](https://github.com/spyder-ide/qtsass/issues/9) - Add MIT LICENSE.txt file ([PR 2](https://github.com/spyder-ide/qtsass/pull/2) by [@danbradham](https://github.com/danbradham)) * [Issue 8](https://github.com/spyder-ide/qtsass/issues/8) - Add Travis CI integration for building and running tests ([PR 2](https://github.com/spyder-ide/qtsass/pull/2) by [@danbradham](https://github.com/danbradham)) * [Issue 7](https://github.com/spyder-ide/qtsass/issues/7) - Create RELEASE.md instructions ([PR 32](https://github.com/spyder-ide/qtsass/pull/32) by [@goanpeca](https://github.com/goanpeca)) * [Issue 6](https://github.com/spyder-ide/qtsass/issues/6) - Create conda-forge package * [Issue 5](https://github.com/spyder-ide/qtsass/issues/5) - Create a changelog ([PR 32](https://github.com/spyder-ide/qtsass/pull/32) by [@goanpeca](https://github.com/goanpeca)) * [Issue 4](https://github.com/spyder-ide/qtsass/issues/4) - Include @import support ([PR 2](https://github.com/spyder-ide/qtsass/pull/2) by [@danbradham](https://github.com/danbradham)) * [Issue 3](https://github.com/spyder-ide/qtsass/issues/3) - Make package pip installable ([PR 2](https://github.com/spyder-ide/qtsass/pull/2) by [@danbradham](https://github.com/danbradham)) In this release 14 issues were closed. ### Pull Requests Merged * [PR 32](https://github.com/spyder-ide/qtsass/pull/32) - PR: Prepare release, by [@goanpeca](https://github.com/goanpeca) ([7](https://github.com/spyder-ide/qtsass/issues/7), [5](https://github.com/spyder-ide/qtsass/issues/5), [17](https://github.com/spyder-ide/qtsass/issues/17)) * [PR 23](https://github.com/spyder-ide/qtsass/pull/23) - PR: Add support for compiling directories of QtSass, by [@danbradham](https://github.com/danbradham) ([18](https://github.com/spyder-ide/qtsass/issues/18)) * [PR 19](https://github.com/spyder-ide/qtsass/pull/19) - PR: windows ci - appveyor, by [@danbradham](https://github.com/danbradham) ([16](https://github.com/spyder-ide/qtsass/issues/16)) * [PR 15](https://github.com/spyder-ide/qtsass/pull/15) - PR: Add code coverage, by [@goanpeca](https://github.com/goanpeca) ([10](https://github.com/spyder-ide/qtsass/issues/10)) * [PR 14](https://github.com/spyder-ide/qtsass/pull/14) - PR: Add badges, by [@danbradham](https://github.com/danbradham) ([11](https://github.com/spyder-ide/qtsass/issues/11)) * [PR 13](https://github.com/spyder-ide/qtsass/pull/13) - PR: Moved tests to qtsass/tests/, by [@danbradham](https://github.com/danbradham) ([12](https://github.com/spyder-ide/qtsass/issues/12)) * [PR 2](https://github.com/spyder-ide/qtsass/pull/2) - PR: Make project pip installable and add @import support, by [@danbradham](https://github.com/danbradham) ([9](https://github.com/spyder-ide/qtsass/issues/9), [8](https://github.com/spyder-ide/qtsass/issues/8), [4](https://github.com/spyder-ide/qtsass/issues/4), [3](https://github.com/spyder-ide/qtsass/issues/3)) In this release 7 pull requests were closed. qtsass-0.4.0/LICENSE.txt000066400000000000000000000021421441041562100146510ustar00rootroot00000000000000MIT License Copyright (c) 2015 Yann Lanthony Copyright (c) 2017-2018 Spyder Project Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. qtsass-0.4.0/MANIFEST.in000066400000000000000000000002051441041562100145620ustar00rootroot00000000000000# Include the README include *.md # Include the license file include LICENSE.txt # Include the data files recursive-include data * qtsass-0.4.0/README.md000066400000000000000000000157401441041562100143150ustar00rootroot00000000000000# QtSASS: Compile SCSS files to Qt stylesheets [![License - MIT](https://img.shields.io/github/license/spyder-ide/qtsass.svg)](./LICENSE.txt) [![OpenCollective Backers](https://opencollective.com/spyder/backers/badge.svg?color=blue)](#backers) [![Join the chat at https://gitter.im/spyder-ide/public](https://badges.gitter.im/spyder-ide/spyder.svg)](https://gitter.im/spyder-ide/public)
[![Github build status](https://github.com/spyder-ide/qtsass/workflows/Tests/badge.svg)](https://github.com/spyder-ide/qtsass/actions) [![Codecov coverage](https://img.shields.io/codecov/c/github/spyder-ide/qtsass/master.svg)](https://codecov.io/gh/spyder-ide/qtsass) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/spyder-ide/qtsass/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/spyder-ide/qtsass/?branch=master) *Copyright © 2015 Yann Lanthony* *Copyright © 2017–2018 Spyder Project Contributors* ## Overview [SASS](http://sass-lang.com/) brings countless amazing features to CSS. Besides being used in web development, CSS is also the way to stylize Qt-based desktop applications. However, Qt's CSS has a few variations that prevent the direct use of SASS compiler. The purpose of this tool is to fill the gap between SASS and Qt-CSS by handling those variations. ## Qt's CSS specificities The goal of QtSASS is to be able to generate a Qt-CSS stylesheet based on a 100% valid SASS file. This is how it deals with Qt's specifics and how you should modify your CSS stylesheet to use QtSASS. #### "!" in selectors Qt allows to define the style of a widget according to its states, like this: ```css QLineEdit:enabled { ... } ``` However, a "not" state is problematic because it introduces an exclamation mark in the selector's name, which is not valid SASS/CSS: ```css QLineEdit:!editable { ... } ``` QtSASS allows "!" in selectors' names; the SASS file is preprocessed and any occurence of `:!` is replaced by `:_qnot_` (for "Qt not"). However, using this feature prevents from having a 100% valid SASS file, so this support of `!` might change in the future. This can be replaced by the direct use of the `_qnot_` keyword in your SASS file: ```css QLineEdit:_qnot_editable { /* will generate QLineEdit:!editable { */ ... } ``` #### qlineargradient The qlineargradient function also has a non-valid CSS syntax. ```css qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0.1 blue, stop: 0.8 green) ``` To support qlineargradient QtSASS provides a preprocessor and a SASS implementation of the qlineargradient function. The above QSS syntax will be replaced with the following: ```css qlineargradient(0, 0, 0, 1, (0.1 blue, 0.8 green)) ``` You may also use this syntax directly in your QtSASS. ``` qlineargradient(0, 0, 0, 1, (0.1 blue, 0.8 green)) # the stops parameter is a list, so you can also use variables: $stops = 0.1 blue, 0.8 green qlineargradient(0, 0, 0, 0, $stops) ``` #### qrgba Qt's rgba: ```css rgba(255, 128, 128, 50%) ``` is replaced by CSS rgba: ```css rgba(255, 128, 128, 0.5) ``` ## Executable usage To compile your SASS stylesheet to a Qt compliant CSS file: ```bash # If -o is omitted, output will be printed to console qtsass style.scss -o style.css ``` To use the watch mode and get your stylesheet auto recompiled on each file save: ```bash # If -o is omitted, output will be print to console qtsass style.scss -o style.css -w ``` To compile a directory containing SASS stylesheets to Qt compliant CSS files: ```bash qtsass ./static/scss -o ./static/css ``` You can also use watch mode to watch the entire directory for changes. ```bash qtsass ./static/scss -o ./static/css -w ``` Set the Environment Variable QTSASS_DEBUG to 1 or pass the --debug flag to enable logging. ```bash qtsass ./static/scss -o ./static/css --debug ``` ## API methods ### `compile(string, **kwargs)` Conform and Compile QtSASS source code to CSS. This function conforms QtSASS to valid SCSS before passing it to sass.compile. Any keyword arguments you provide will be combined with qtsass's default keyword arguments and passed to sass.compile. Examples: ```bash >>> import qtsass >>> qtsass.compile("QWidget {background: rgb(0, 0, 0);}") QWidget {background:black;} ``` Arguments: - string: QtSASS source code to conform and compile. - kwargs: Keyword arguments to pass to sass.compile Returns: - Qt compliant CSS string ### `compile_filename(input_file, output_file=None, **kwargs)`: Compile and return a QtSASS file as Qt compliant CSS. Optionally save to a file. Examples: ```bash >>> import qtsass >>> qtsass.compile_filename("dummy.scss", "dummy.css") >>> css = qtsass.compile_filename("dummy.scss") ``` Arguments: - input_file: Path to QtSass file. - output_file: Path to write Qt compliant CSS. - kwargs: Keyword arguments to pass to sass.compile Returns: - Qt compliant CSS string ### `compile_dirname(input_dir, output_dir, **kwargs)`: Compiles QtSASS files in a directory including subdirectories. ```bash >>> import qtsass >>> qtsass.compile_dirname("./scss", "./css") ``` Arguments: - input_dir: Path to directory containing QtSass files. - output_dir: Directory to write compiled Qt compliant CSS files to. - kwargs: Keyword arguments to pass to sass.compile ### `enable_logging(level=None, handler=None)`: Enable logging for qtsass. Sets the qtsass logger's level to: 1. the provided logging level 2. logging.DEBUG if the QTSASS_DEBUG envvar is a True value 3. logging.WARNING ```bash >>> import logging >>> import qtsass >>> handler = logging.StreamHandler() >>> formatter = logging.Formatter('%(level)-8s: %(name)s> %(message)s') >>> handler.setFormatter(formatter) >>> qtsass.enable_logging(level=logging.DEBUG, handler=handler) ``` Arguments: - level: Optional logging level - handler: Optional handler to add ### `watch(source, destination, compiler=None, Watcher=None)`: Watches a source file or directory, compiling QtSass files when modified. The compiler function defaults to compile_filename when source is a file and compile_dirname when source is a directory. Arguments: - source: Path to source QtSass file or directory. - destination: Path to output css file or directory. - compiler: Compile function (optional) - Watcher: Defaults to qtsass.watchers.Watcher (optional) Returns: - qtsass.watchers.Watcher instance ## Contributing Everyone is welcome to contribute! ## Sponsors Spyder and its subprojects are funded thanks to the generous support of [![Quansight](https://static.wixstatic.com/media/095d2c_2508c560e87d436ea00357abc404cf1d~mv2.png/v1/crop/x_0,y_9,w_915,h_329/fill/w_380,h_128,al_c,usm_0.66_1.00_0.01/095d2c_2508c560e87d436ea00357abc404cf1d~mv2.png)](https://www.quansight.com/)[![Numfocus](https://i2.wp.com/numfocus.org/wp-content/uploads/2017/07/NumFocus_LRG.png?fit=320%2C148&ssl=1)](https://numfocus.org/) and the donations we have received from our users around the world through [Open Collective](https://opencollective.com/spyder/): [![Sponsors](https://opencollective.com/spyder/sponsors.svg)](https://opencollective.com/spyder#support) Please consider becoming a sponsor! qtsass-0.4.0/RELEASE.md000066400000000000000000000046701441041562100144400ustar00rootroot00000000000000# Release process ## Using rever You need to have `conda` install since the process relies on conda environments. Make sure your current environment has [rever](https://regro.github.io/rever-docs/) installed. ```bash conda install rever -c conda-forge ``` Run checks before to make sure things are in order. ```bash rever check ``` Delete the `rever/` folder to start a clean release. ```bash rm -rf rever/ ``` Run rever with the type version (major|minor|patch|MAJOR.MINOR.PATCH) to update. ### Major release If the current version is `3.0.0.dev0`, running: ```bash rever major ``` Will produce version `4.0.0` and update the dev version to `4.0.0.dev0` ### Minor release If the current version is `3.0.0.dev0`, running: ```bash rever minor ``` Will produce version `3.1.0` and update the dev version to `3.1.0.dev0` ### Patch release If the current version is `3.0.0.dev0`, running: ```bash rever patch ``` Will produce version `3.0.1` and update the dev version to `3.0.1.dev0` ### MAJOR.MINOR.PATCH release If the current version is `3.0.0.dev0`, running: ```bash rever 5.0.1 ``` Will produce version `5.0.1` and update the dev version to `5.0.1.dev0` ### Important - In case some of the steps appear as completed, delete the `rever` folder. ```bash rm -rf rever/ ``` - Some of the intermediate steps may ask for feedback, like checking the changelog. ## Semi-automatic process using Git and GitHub actions - Ensure you have the latest version from upstream and update your fork ```bash git pull upstream master git push origin master ``` - Clean the repo (select option 1) ```bash git clean -xfdi ``` - Update `CHANGELOG.md` using loghub ```bash loghub spyder-ide/qtsass -m ``` - Update version in `__init__.py` (set release version, remove 'dev0') - Commit and push changes ```bash git add . git commit -m "Release X.X.X" git push upstream master git push origin master ``` - Make a [new release](https://github.com/spyder-ide/qtsass/releases) with tag name `vX.X.X` - Check that [the CI workflow](https://github.com/spyder-ide/qtsass/actions) for `vX.X.X` successfully deployed the new release - Update `__init__.py` (add 'dev0' and increment minor) - Commit and push changes ```bash git add . git commit -m "Back to work" git push upstream master git push origin master ``` ## To release a new version of **qtsass** on conda-forge - Update recipe on the [qtsass feedstock](https://github.com/conda-forge/qtsass-feedstock) qtsass-0.4.0/examples/000077500000000000000000000000001441041562100146455ustar00rootroot00000000000000qtsass-0.4.0/examples/complex/000077500000000000000000000000001441041562100163145ustar00rootroot00000000000000qtsass-0.4.0/examples/complex/_base.scss000066400000000000000000000001531441041562100202610ustar00rootroot00000000000000@import 'defaults'; @import 'widgets/qwidget'; @import 'widgets/qpushbutton'; @import 'widgets/qlineedit'; qtsass-0.4.0/examples/complex/_defaults.scss000066400000000000000000000004251441041562100211600ustar00rootroot00000000000000// Fonts $font-stack: Helvetica, sans-serif !default; // Colors $background: rgb(255, 255, 255) !default; $primary: rgb(35, 35, 35) !default; $accent: rgb(35, 75, 135) !default; // Paddings $text-padding: 16px 8px 16px 8px !default; // Borders $border-radius: 2px !default; qtsass-0.4.0/examples/complex/dark.scss000066400000000000000000000001761441041562100201360ustar00rootroot00000000000000// Change default values $background: rgb(35, 35, 35); $primary: rgb(255, 255, 255); // Import base style @import 'base'; qtsass-0.4.0/examples/complex/light.scss000066400000000000000000000000201441041562100203100ustar00rootroot00000000000000@import 'base'; qtsass-0.4.0/examples/complex/widgets/000077500000000000000000000000001441041562100177625ustar00rootroot00000000000000qtsass-0.4.0/examples/complex/widgets/_qlineedit.scss000066400000000000000000000001651441041562100227760ustar00rootroot00000000000000QLineEdit { color: $primary; padding: $text-padding; border: 0; border-bottom: 1px solid $primary; } qtsass-0.4.0/examples/complex/widgets/_qpushbutton.scss000066400000000000000000000003521441041562100234120ustar00rootroot00000000000000QPushButton { background: $background; color: $accent; padding: $text-padding; border: 0; border-radius: $border-radius; } QPushButton:hover, QPushButton:focus { background: $accent; color: $background; } qtsass-0.4.0/examples/complex/widgets/_qwidget.scss000066400000000000000000000000761441041562100224650ustar00rootroot00000000000000QWidget { background: $background; color: $primary; } qtsass-0.4.0/examples/dummy.scss000066400000000000000000000013461441041562100167010ustar00rootroot00000000000000 // This comment will not appear in generated css file /* This comment will appear in generated css file */ // !editable is not valid sass/css but is tolerated in qtsass since widespread in Qt stylesheets. QComboBox:!editable:on, QComboBox::drop-down:editable:on { color: blue; } // standard qss qlineargradient syntax works QListView::item:selected{ background-color: qlineargradient( x1: 0, y1: 0, x2: 0, y2:1, stop: 0.2 #3f3f3f, stop: 0.8 red ); } // You may also use QtSASS syntax directly QTreeView::item:selected{ $start: 0.2; $stops: $start #3f3f3f, $start + 0.6 red; background-color: qlineargradient(0, 0, 0, 1, $stops); color: rgba(255, 10, 10, 0.5); } qtsass-0.4.0/qtsass/000077500000000000000000000000001441041562100143455ustar00rootroot00000000000000qtsass-0.4.0/qtsass/__init__.py000066400000000000000000000026121441041562100164570ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """ The SASS language brings countless amazing features to CSS. Besides being used in web development, CSS is also the way to stylize Qt-based desktop applications. However, Qt's CSS has a few variations that prevent the direct use of SASS compiler. The purpose of qtsass is to fill the gap between SASS and Qt-CSS by handling those variations. """ # yapf: disable from __future__ import absolute_import # Standard library imports import logging # Local imports from qtsass.api import ( compile, compile_dirname, compile_filename, enable_logging, watch, ) # yapf: enable # Setup Logging logging.getLogger(__name__).addHandler(logging.NullHandler()) enable_logging() # Constants __version__ = '0.4.0' def _to_version_info(version): """Convert a version string to a number and string tuple.""" parts = [] for part in version.split('.'): try: part = int(part) except ValueError: pass parts.append(part) return tuple(parts) VERSION_INFO = _to_version_info(__version__) qtsass-0.4.0/qtsass/__main__.py000066400000000000000000000010711441041562100164360ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """qtsass command line interface.""" # yapf: disable from __future__ import absolute_import # Local imports from qtsass import cli # yapf: enable if __name__ == '__main__': cli.main() qtsass-0.4.0/qtsass/api.py000066400000000000000000000175141441041562100155000ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """qtsass - Compile SCSS files to valid Qt stylesheets.""" # yapf: disable from __future__ import absolute_import, print_function # Standard library imports from collections.abc import Mapping, Sequence import logging import os # Third party imports import sass # Local imports from qtsass.conformers import qt_conform, scss_conform from qtsass.functions import qlineargradient, qradialgradient, rgba from qtsass.importers import qss_importer # yapf: enable # Constants DEFAULT_CUSTOM_FUNCTIONS = { 'qlineargradient': qlineargradient, 'qradialgradient': qradialgradient, 'rgba': rgba } DEFAULT_SOURCE_COMMENTS = False # Logger setup _log = logging.getLogger(__name__) def compile(string, **kwargs): """ Conform and Compile QtSASS source code to CSS. This function conforms QtSASS to valid SCSS before passing it to sass.compile. Any keyword arguments you provide will be combined with qtsass's default keyword arguments and passed to sass.compile. .. code-block:: python >>> import qtsass >>> qtsass.compile("QWidget {background: rgb(0, 0, 0);}") QWidget {background:black;} :param string: QtSASS source code to conform and compile. :param kwargs: Keyword arguments to pass to sass.compile :returns: CSS string """ kwargs.setdefault('source_comments', DEFAULT_SOURCE_COMMENTS) kwargs.setdefault('custom_functions', []) kwargs.setdefault('importers', []) kwargs.setdefault('include_paths', []) # Add QtSass importers if isinstance(kwargs['importers'], Sequence): kwargs['importers'] = (list(kwargs['importers']) + [(0, qss_importer(*kwargs['include_paths']))]) else: raise ValueError('Expected Sequence for importers ' 'got {}'.format(type(kwargs['importers']))) # Add QtSass custom_functions if isinstance(kwargs['custom_functions'], Sequence): kwargs['custom_functions'] = dict( DEFAULT_CUSTOM_FUNCTIONS, **{fn.__name__: fn for fn in kwargs['custom_functions']}) elif isinstance(kwargs['custom_functions'], Mapping): kwargs['custom_functions'].update(DEFAULT_CUSTOM_FUNCTIONS) else: raise ValueError('Expected Sequence or Mapping for custom_functions ' 'got {}'.format(type(kwargs['custom_functions']))) # Conform QtSass source code try: kwargs['string'] = scss_conform(string) except Exception: _log.error('Failed to conform source code') raise if _log.isEnabledFor(logging.DEBUG): from pprint import pformat log_kwargs = dict(kwargs) log_kwargs['string'] = 'Conformed SCSS<...>' _log.debug('Calling sass.compile with:') _log.debug(pformat(log_kwargs)) _log.debug('Conformed scss:\n{}'.format(kwargs['string'])) # Compile QtSass source code try: return qt_conform(sass.compile(**kwargs)) except sass.CompileError: _log.error('Failed to compile source code') raise def compile_filename(input_file, output_file=None, **kwargs): """Compile and return a QtSASS file as Qt compliant CSS. Optionally save to a file. .. code-block:: python >>> import qtsass >>> qtsass.compile_filename("dummy.scss", "dummy.css") >>> css = qtsass.compile_filename("dummy.scss") :param input_file: Path to QtSass file. :param output_file: Optional path to write Qt compliant CSS. :param kwargs: Keyword arguments to pass to sass.compile :returns: CSS string """ input_root = os.path.abspath(os.path.dirname(input_file)) kwargs.setdefault('include_paths', [input_root]) with open(input_file, 'r') as f: string = f.read() _log.info('Compiling {}...'.format(os.path.normpath(input_file))) css = compile(string, **kwargs) if output_file is not None: output_root = os.path.abspath(os.path.dirname(output_file)) if not os.path.isdir(output_root): os.makedirs(output_root) with open(output_file, 'w') as css_file: css_file.write(css) _log.info('Created CSS file {}'.format( os.path.normpath(output_file))) return css def compile_dirname(input_dir, output_dir, **kwargs): """Compiles QtSASS files in a directory including subdirectories. .. code-block:: python >>> import qtsass >>> qtsass.compile_dirname("./scss", "./css") :param input_dir: Directory containing QtSass files. :param output_dir: Directory to write compiled Qt compliant CSS files to. :param kwargs: Keyword arguments to pass to sass.compile """ kwargs.setdefault('include_paths', [input_dir]) def is_valid(file_name): return not file_name.startswith('_') and file_name.endswith('.scss') for root, _, files in os.walk(input_dir): relative_root = os.path.relpath(root, input_dir) output_root = os.path.join(output_dir, relative_root) fkwargs = dict(kwargs) fkwargs['include_paths'] = fkwargs['include_paths'] + [root] for file_name in [f for f in files if is_valid(f)]: scss_path = os.path.join(root, file_name) css_file = os.path.splitext(file_name)[0] + '.css' css_path = os.path.join(output_root, css_file) if not os.path.isdir(output_root): os.makedirs(output_root) compile_filename(scss_path, css_path, **fkwargs) def enable_logging(level=None, handler=None): """Enable logging for qtsass. Sets the qtsass logger's level to: 1. the provided logging level 2. logging.DEBUG if the QTSASS_DEBUG envvar is a True value 3. logging.WARNING .. code-block:: python >>> import logging >>> import qtsass >>> handler = logging.StreamHandler() >>> formatter = logging.Formatter('%(level)-8s: %(name)s> %(message)s') >>> handler.setFormatter(formatter) >>> qtsass.enable_logging(level=logging.DEBUG, handler=handler) :param level: Optional logging level :param handler: Optional handler to add """ if level is None: debug = os.environ.get('QTSASS_DEBUG', False) if debug in ('1', 'true', 'True', 'TRUE', 'on', 'On', 'ON'): level = logging.DEBUG else: level = logging.WARNING logger = logging.getLogger('qtsass') logger.setLevel(level) if handler: logger.addHandler(handler) _log.debug('logging level set to {}.'.format(level)) def watch(source, destination, compiler=None, Watcher=None): """ Watches a source file or directory, compiling QtSass files when modified. The compiler function defaults to compile_filename when source is a file and compile_dirname when source is a directory. :param source: Path to source QtSass file or directory. :param destination: Path to output css file or directory. :param compiler: Compile function (optional) :param Watcher: Defaults to qtsass.watchers.Watcher (optional) :returns: qtsass.watchers.Watcher instance """ if os.path.isfile(source): watch_dir = os.path.dirname(source) compiler = compiler or compile_filename elif os.path.isdir(source): watch_dir = source compiler = compiler or compile_dirname else: raise ValueError('source arg must be a dirname or filename...') if Watcher is None: from qtsass.watchers import Watcher watcher = Watcher(watch_dir, compiler, (source, destination)) return watcher qtsass-0.4.0/qtsass/cli.py000066400000000000000000000064161441041562100154750ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """qtsass command line interface.""" # yapf: disable from __future__ import absolute_import, print_function # Standard library imports import argparse import logging import os import sys import time # Local imports from qtsass.api import ( compile, compile_dirname, compile_filename, enable_logging, watch, ) # yapf: enable _log = logging.getLogger(__name__) def create_parser(): """Create qtsass's cli parser.""" parser = argparse.ArgumentParser( prog='QtSASS', description='Compile a Qt compliant CSS file from a SASS stylesheet.', ) parser.add_argument( 'input', type=str, help='The SASS stylesheet file.', ) parser.add_argument( '-o', '--output', type=str, help='The path of the generated Qt compliant CSS file.', ) parser.add_argument( '-w', '--watch', action='store_true', help='If set, recompile when the source file changes.', ) parser.add_argument( '-d', '--debug', action='store_true', help='Set the logging level to DEBUG.', ) return parser def main(): """CLI entry point.""" args = create_parser().parse_args() # Setup CLI logging debug = os.environ.get('QTSASS_DEBUG', args.debug) if debug in ('1', 'true', 'True', 'TRUE', 'on', 'On', 'ON', True): level = logging.DEBUG else: level = logging.INFO enable_logging(level) # Add a StreamHandler handler = logging.StreamHandler() if level == logging.DEBUG: fmt = '%(levelname)-8s: %(name)s> %(message)s' handler.setFormatter(logging.Formatter(fmt)) logging.root.addHandler(handler) logging.root.setLevel(level) file_mode = os.path.isfile(args.input) dir_mode = os.path.isdir(args.input) if file_mode and not args.output: with open(args.input, 'r') as f: string = f.read() css = compile( string, include_paths=os.path.abspath(os.path.dirname(args.input)), ) print(css) sys.exit(0) elif file_mode: _log.debug('compile_filename({}, {})'.format(args.input, args.output)) compile_filename(args.input, args.output) elif dir_mode and not args.output: print('Error: missing required option: -o/--output') sys.exit(1) elif dir_mode: _log.debug('compile_dirname({}, {})'.format(args.input, args.output)) compile_dirname(args.input, args.output) else: print('Error: input must be a file or a directory') sys.exit(1) if args.watch: _log.info('qtsass is watching {}...'.format(args.input)) watcher = watch(args.input, args.output) watcher.start() try: while True: time.sleep(0.5) except KeyboardInterrupt: watcher.stop() watcher.join() sys.exit(0) qtsass-0.4.0/qtsass/conformers.py000066400000000000000000000200151441041562100170720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Conform qss to compliant scss and css to valid qss.""" # yapf: disable from __future__ import absolute_import, print_function # Standard library imports import re # yapf: enable class Conformer(object): """Base class for all text transformations.""" def to_scss(self, qss): """Transform some qss to valid scss.""" return NotImplemented def to_qss(self, css): """Transform some css to valid qss.""" return NotImplemented class NotConformer(Conformer): """Conform QSS "!" in selectors.""" def to_scss(self, qss): """Replace "!" in selectors with "_qnot_".""" return qss.replace(':!', ':_qnot_') def to_qss(self, css): """Replace "_qnot_" in selectors with "!".""" return css.replace(':_qnot_', ':!') class QLinearGradientConformer(Conformer): """Conform QSS qlineargradient function.""" _DEFAULT_COORDS = ('x1', 'y1', 'x2', 'y2') qss_pattern = re.compile( r'qlineargradient\(' r'((?:(?:\s+)?(?:x1|y1|x2|y2):(?:\s+)?[0-9A-Za-z$_\.-]+,?)+)' # coords r'((?:(?:\s+)?stop:.*,?)+(?:\s+)?)?' # stops r'\)', re.MULTILINE, ) def _conform_coords_to_scss(self, group): """ Take a qss str with xy coords and returns the values. 'x1: 0, y1: 0, x2: 0, y2: 0' => '0, 0, 0, 0' 'y1: 1' => '0, 1, 0, 0' """ values = ['0', '0', '0', '0'] for key_values in [part.split(':', 1) for part in group.split(',')]: try: key, value = key_values key = key.strip() if key in self._DEFAULT_COORDS: pos = self._DEFAULT_COORDS.index(key) if pos >= 0 and pos <= 3: values[pos] = value.strip() except ValueError: pass return ', '.join(values) def _conform_stops_to_scss(self, group): """ Take a qss str with stops and returns the values. 'stop: 0 red, stop: 1 blue' => '0 red, 1 blue' """ new_group = [] split = [""] bracket_level = 0 for char in group: if not bracket_level and char == ",": split.append("") continue elif char == "(": bracket_level += 1 elif char == ")": bracket_level -= 1 split[-1] += char for part in split: if part: _, value = part.split(':', 1) new_group.append(value.strip()) return ', '.join(new_group) def to_scss(self, qss): """ Conform qss qlineargradient to scss qlineargradient form. Normalize all whitespace including the removal of newline chars. qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0, stop: 0 red, stop: 1 blue) => qlineargradient(0, 0, 0, 0, (0 red, 1 blue)) """ conformed = qss for coords, stops in self.qss_pattern.findall(qss): new_coords = self._conform_coords_to_scss(coords) conformed = conformed.replace(coords, new_coords, 1) if not stops: continue new_stops = ', ({})'.format(self._conform_stops_to_scss(stops)) conformed = conformed.replace(stops, new_stops, 1) return conformed def to_qss(self, css): """Transform to qss from css.""" return css class QRadialGradientConformer(Conformer): """Conform QSS qradialgradient function.""" _DEFAULT_COORDS = ('cx', 'cy', 'radius', 'fx', 'fy') qss_pattern = re.compile( r'qradialgradient\(' # spread r'((?:(?:\s+)?(?:spread):(?:\s+)?[0-9A-Za-z$_\.-]+,?)+)?' # coords r'((?:(?:\s+)?(?:cx|cy|radius|fx|fy):(?:\s+)?[0-9A-Za-z$_\.-]+,?)+)' # stops r'((?:(?:\s+)?stop:.*,?)+(?:\s+)?)?' r'\)', re.MULTILINE, ) def _conform_spread_to_scss(self, group): """ Take a qss str with xy coords and returns the values. 'spread: pad|repeat|reflect' """ value = 'pad' for key_values in [part.split(':', 1) for part in group.split(',')]: try: key, value = key_values key = key.strip() if key == 'spread': value = value.strip() except ValueError: pass return value def _conform_coords_to_scss(self, group): """ Take a qss str with xy coords and returns the values. 'cx: 0, cy: 0, radius: 0, fx: 0, fy: 0' => '0, 0, 0, 0, 0' 'cy: 1' => '0, 1, 0, 0, 0' """ values = ['0', '0', '0', '0', '0'] for key_values in [part.split(':', 1) for part in group.split(',')]: try: key, value = key_values key = key.strip() if key in self._DEFAULT_COORDS: pos = self._DEFAULT_COORDS.index(key) if pos >= 0: values[pos] = value.strip() except ValueError: pass return ', '.join(values) def _conform_stops_to_scss(self, group): """ Take a qss str with stops and returns the values. 'stop: 0 red, stop: 1 blue' => '0 red, 1 blue' """ new_group = [] split = [""] bracket_level = 0 for char in group: if not bracket_level and char == ",": split.append("") continue elif char == "(": bracket_level += 1 elif char == ")": bracket_level -= 1 split[-1] += char for part in split: if part: _, value = part.split(':', 1) new_group.append(value.strip()) return ', '.join(new_group) def to_scss(self, qss): """ Conform qss qradialgradient to scss qradialgradient form. Normalize all whitespace including the removal of newline chars. qradialgradient(cx: 0, cy: 0, radius: 0, fx: 0, fy: 0, stop: 0 red, stop: 1 blue) => qradialgradient(0, 0, 0, 0, 0, (0 red, 1 blue)) """ conformed = qss for spread, coords, stops in self.qss_pattern.findall(qss): new_spread = "'" + self._conform_spread_to_scss(spread) + "', " conformed = conformed.replace(spread, new_spread, 1) new_coords = self._conform_coords_to_scss(coords) conformed = conformed.replace(coords, new_coords, 1) if not stops: continue new_stops = ', ({})'.format(self._conform_stops_to_scss(stops)) conformed = conformed.replace(stops, new_stops, 1) return conformed def to_qss(self, css): """Transform to qss from css.""" return css conformers = [c() for c in Conformer.__subclasses__() if c is not Conformer] def scss_conform(input_str): """ Conform qss to valid scss. Runs the to_scss method of all Conformer subclasses on the input_str. Conformers are run in order of definition. :param input_str: QSS string :returns: Valid SCSS string """ conformed = input_str for conformer in conformers: conformed = conformer.to_scss(conformed) return conformed def qt_conform(input_str): """ Conform css to valid qss. Runs the to_qss method of all Conformer subclasses on the input_str. Conformers are run in reverse order. :param input_str: CSS string :returns: Valid QSS string """ conformed = input_str for conformer in conformers[::-1]: conformed = conformer.to_qss(conformed) return conformed qtsass-0.4.0/qtsass/functions.py000066400000000000000000000060301441041562100167260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Libsass functions.""" # yapf: disable # Third party imports import sass # yapf: enable def rgba(r, g, b, a): """Convert r,g,b,a values to standard format. Where `a` is alpha! In CSS alpha can be given as: * float from 0.0 (fully transparent) to 1.0 (opaque) In Qt or qss that is: * int from 0 (fully transparent) to 255 (opaque) A percentage value 0% (fully transparent) to 100% (opaque) works in BOTH systems the same way! """ result = 'rgba({}, {}, {}, {}%)' if isinstance(r, sass.SassNumber): if a.unit == '%': alpha = a.value elif a.value > 1.0: # A value from 0 to 255 is coming in, convert to % alpha = a.value / 2.55 else: alpha = a.value * 100 return result.format( int(r.value), int(g.value), int(b.value), int(alpha), ) elif isinstance(r, float): return result.format(int(r), int(g), int(b), int(a * 100)) def rgba_from_color(color): """ Conform rgba. :type color: sass.SassColor """ # Inner rgba() call if not isinstance(color, sass.SassColor): return '{}'.format(color) return rgba(color.r, color.g, color.b, color.a) def qlineargradient(x1, y1, x2, y2, stops): """ Implement qss qlineargradient function for scss. :type x1: sass.SassNumber :type y1: sass.SassNumber :type x2: sass.SassNumber :type y2: sass.SassNumber :type stops: sass.SassList :return: """ stops_str = [] for stop in stops[0]: pos, color = stop[0] stops_str.append('stop: {} {}'.format( pos.value, rgba_from_color(color), )) template = 'qlineargradient(x1: {}, y1: {}, x2: {}, y2: {}, {})' return template.format(x1.value, y1.value, x2.value, y2.value, ', '.join(stops_str)) def qradialgradient(spread, cx, cy, radius, fx, fy, stops): """ Implement qss qradialgradient function for scss. :type spread: string :type cx: sass.SassNumber :type cy: sass.SassNumber :type radius: sass.SassNumber :type fx: sass.SassNumber :type fy: sass.SassNumber :type stops: sass.SassList :return: """ stops_str = [] for stop in stops[0]: pos, color = stop[0] stops_str.append('stop: {} {}'.format( pos.value, rgba_from_color(color), )) template = ('qradialgradient(' 'spread: {}, cx: {}, cy: {}, radius: {}, fx: {}, fy: {}, {}' ')') return template.format(spread, cx.value, cy.value, radius.value, fx.value, fy.value, ', '.join(stops_str)) qtsass-0.4.0/qtsass/importers.py000066400000000000000000000043401441041562100167440ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Libsass importers.""" # yapf: disable from __future__ import absolute_import # Standard library imports import os # Local imports from qtsass.conformers import scss_conform # yapf: enable def norm_path(*parts): """Normalize path.""" return os.path.normpath(os.path.join(*parts)).replace('\\', '/') def qss_importer(*include_paths): """ Return function which conforms imported qss files to valid scss. This fucntion is to be used as an importer for sass.compile. :param include_paths: Directorys containing scss, css, and sass files. """ include_paths def find_file(import_file): # Create partial import filename dirname, basename = os.path.split(import_file) if dirname: import_partial_file = '/'.join([dirname, '_' + basename]) else: import_partial_file = '_' + basename # Build potential file paths for @import "import_file" potential_files = [] for ext in ['', '.scss', '.css', '.sass']: full_name = import_file + ext partial_name = import_partial_file + ext potential_files.append(full_name) potential_files.append(partial_name) for path in include_paths: potential_files.append(norm_path(path, full_name)) potential_files.append(norm_path(path, partial_name)) # Return first existing potential file for potential_file in potential_files: if os.path.isfile(potential_file): return potential_file return None def import_and_conform_file(import_file): """Return base file and conformed scss file.""" real_import_file = find_file(import_file) with open(real_import_file, 'r') as f: import_str = f.read() return [(import_file, scss_conform(import_str))] return import_and_conform_file qtsass-0.4.0/qtsass/watchers/000077500000000000000000000000001441041562100161655ustar00rootroot00000000000000qtsass-0.4.0/qtsass/watchers/__init__.py000066400000000000000000000014361441041562100203020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """The qtsass Watcher is responsible for watching and recompiling sass. The default Watcher is the QtWatcher. If Qt is unavailable we fallback to the PollingWatcher. """ # yapf: disable from __future__ import absolute_import # Local imports from qtsass.watchers.polling import PollingWatcher try: from qtsass.watchers.qt import QtWatcher except ImportError: QtWatcher = None # yapf: enable Watcher = QtWatcher or PollingWatcher qtsass-0.4.0/qtsass/watchers/api.py000066400000000000000000000103221441041562100173060ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """The filesystem watcher api.""" # yapf: disable from __future__ import absolute_import # Standard library imports import functools import logging import time _log = logging.getLogger(__name__) def retry(n, interval=0.1): """Retry a function or method n times before raising an exception. :param n: Number of times to retry :param interval: Time to sleep before attempts """ def decorate(fn): @functools.wraps(fn) def attempt(*args, **kwargs): attempts = 0 while True: try: return fn(*args, **kwargs) except Exception: attempts += 1 if n <= attempts: raise time.sleep(interval) return attempt return decorate # yapf: enable class Watcher(object): """Watcher base class. Watchers monitor a file or directory and call the on_change method when a change occurs. The on_change method should trigger the compiler function passed in during construction and dispatch the result to all connected callbacks. Watcher implementations must inherit from this base class. Subclasses should perform any setup required in the setup method, rather than overriding __init__. """ def __init__(self, watch_dir, compiler, args=None, kwargs=None): """Store initialization values and call Watcher.setup.""" self._watch_dir = watch_dir self._compiler = compiler self._args = args or () self._kwargs = kwargs or {} self._callbacks = set() self._log = _log self.setup() def setup(self): """Perform any setup required here. Rather than implement __init__, subclasses can perform any setup in this method. """ return NotImplemented def start(self): """Start this Watcher.""" return NotImplemented def stop(self): """Stop this Watcher.""" return NotImplemented def join(self): """Wait for this Watcher to finish.""" return NotImplemented @retry(5) def compile(self): """Call the Watcher's compiler.""" self._log.debug( 'Compiling sass...%s(*%s, **%s)', self._compiler, self._args, self._kwargs, ) return self._compiler(*self._args, **self._kwargs) def compile_and_dispatch(self): """Compile and dispatch the resulting css to connected callbacks.""" self._log.debug('Compiling and dispatching....') try: css = self.compile() except Exception: self._log.exception('Failed to compile...') return self.dispatch(css) def dispatch(self, css): """Dispatch css to connected callbacks.""" self._log.debug('Dispatching callbacks...') for callback in self._callbacks: callback(css) def on_change(self): """Call when a change is detected. Subclasses must call this method when they detect a change. Subclasses may also override this method in order to manually compile and dispatch callbacks. For example, a Qt implementation may use signals and slots to ensure that compiling and executing callbacks happens in the main GUI thread. """ self._log.debug('Change detected...') self.compile_and_dispatch() def connect(self, fn): """Connect a callback to this Watcher. All callbacks are called when a change is detected. Callbacks are passed the compiled css. """ self._log.debug('Connecting callback: %s', fn) self._callbacks.add(fn) def disconnect(self, fn): """Disconnect a callback from this Watcher.""" self._log.debug('Disconnecting callback: %s', fn) self._callbacks.discard(fn) qtsass-0.4.0/qtsass/watchers/polling.py000066400000000000000000000071671441041562100202160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Contains the fallback implementation of the Watcher api.""" # yapf: disable from __future__ import absolute_import, print_function # Standard library imports import atexit import threading # Local imports from qtsass.watchers import snapshots from qtsass.watchers.api import Watcher # yapf: enable class PollingThread(threading.Thread): """A thread that fires a callback at an interval.""" def __init__(self, callback, interval): """Initialize the thread. :param callback: Callback function to repeat. :param interval: Number of seconds to sleep between calls. """ super(PollingThread, self).__init__() self.daemon = True self.callback = callback self.interval = interval self._shutdown = threading.Event() self._stopped = threading.Event() self._started = threading.Event() atexit.register(self.stop) @property def started(self): """Check if the thread has started.""" return self._started.is_set() @property def stopped(self): """Check if the thread has stopped.""" return self._stopped.is_set() @property def shutdown(self): """Check if the thread has shutdown.""" return self._shutdown.is_set() def stop(self): """Set the shutdown event for this thread and wait for it to stop.""" if not self.started and not self.shutdown: return self._shutdown.set() self._stopped.wait() def run(self): """Threads main loop.""" try: self._started.set() while True: self.callback() if self._shutdown.wait(self.interval): break finally: self._stopped.set() class PollingWatcher(Watcher): """Polls a directory recursively for changes. Detects file and directory changes, deletions, and creations. Recursion depth is limited to 2 levels. We use a limit because the scss file we're watching for changes could be sitting in the root of a project rather than a dedicated scss directory. That could lead to snapshots taking too long to build and diff. It's probably safe to assume that users aren't nesting scss deeper than a couple of levels. """ def setup(self): """Set up the PollingWatcher. A PollingThread is created but not started. """ self._snapshot_depth = 2 self._snapshot = snapshots.take(self._watch_dir, self._snapshot_depth) self._thread = PollingThread(self.run, interval=1) def start(self): """Start the PollingThread.""" self._thread.start() def stop(self): """Stop the PollingThread.""" self._thread.stop() def join(self): """Wait for the PollingThread to finish. You should always call stop before join. """ self._thread.join() def run(self): """Take a new snapshot and call on_change when a change is detected. Called repeatedly by the PollingThread. """ next_snapshot = snapshots.take(self._watch_dir, self._snapshot_depth) changes = snapshots.diff(self._snapshot, next_snapshot) if changes: self._snapshot = next_snapshot self.on_change() qtsass-0.4.0/qtsass/watchers/qt.py000066400000000000000000000057021441041562100171670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Contains the Qt implementation of the Watcher api.""" # yapf: disable from __future__ import absolute_import # Local imports from qtsass.watchers.polling import PollingWatcher # We cascade through Qt bindings here rather than relying on a comprehensive # Qt compatability library like qtpy or Qt.py. This prevents us from forcing a # specific compatability library on users. QT_BINDING = None if not QT_BINDING: try: from PySide2.QtWidgets import QApplication from PySide2.QtCore import QObject, Signal QT_BINDING = 'pyside2' except ImportError: pass if not QT_BINDING: try: from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QObject from PyQt5.QtCore import pyqtSignal as Signal QT_BINDING = 'pyqt5' except ImportError: pass if not QT_BINDING: try: from PySide.QtGui import QApplication from PySide2.QtCore import QObject, Signal QT_BINDING == 'pyside' except ImportError: pass if not QT_BINDING: from PyQt4.QtGui import QApplication from PyQt4.QtCore import QObject from PyQt4.QtCore import pyqtSignal as Signal QT_BINDING == 'pyqt4' # yapf: enable class QtDispatcher(QObject): """Used by QtWatcher to dispatch callbacks in the main ui thread.""" signal = Signal() class QtWatcher(PollingWatcher): """The Qt implementation of the Watcher api. Subclasses PollingWatcher but dispatches :meth:`compile_and_dispatch` using a Qt Signal to ensure that these calls are executed in the main ui thread. We aren't using a QFileSystemWatcher because it fails to report changes in certain circumstances. """ _qt_binding = QT_BINDING def setup(self): """Set up QtWatcher.""" super(QtWatcher, self).setup() self._qtdispatcher = None @property def qtdispatcher(self): """Get the QtDispatcher.""" if self._qtdispatcher is None: self._qtdispatcher = QtDispatcher() self._qtdispatcher.signal.connect(self.compile_and_dispatch) return self._qtdispatcher def on_change(self): """Call when a change is detected.""" self._log.debug('Change detected...') # If a QApplication event loop has not been started # call compile_and_dispatch in the current thread. if not QApplication.instance(): return super(PollingWatcher, self).compile_and_dispatch() # Create and use a QtDispatcher to ensure compile and any # connected callbacks get executed in the main gui thread. self.qtdispatcher.signal.emit() qtsass-0.4.0/qtsass/watchers/snapshots.py000066400000000000000000000035731441041562100205710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Contains the fallback implementation of the Watcher api.""" # yapf: disable from __future__ import absolute_import, print_function # Standard library imports import os # Local imports from qtsass.importers import norm_path # yapf: enable def take(dir_or_file, depth=3): """Return a dict mapping files and folders to their mtimes.""" if os.path.isfile(dir_or_file): path = norm_path(dir_or_file) return {path: os.path.getmtime(path)} if not os.path.isdir(dir_or_file): return {} snapshot = {} base_depth = len(norm_path(dir_or_file).split('/')) for root, subdirs, files in os.walk(dir_or_file): path = norm_path(root) if len(path.split('/')) - base_depth == depth: subdirs[:] = [] snapshot[path] = os.path.getmtime(path) for f in files: path = norm_path(root, f) snapshot[path] = os.path.getmtime(path) return snapshot def diff(prev_snapshot, next_snapshot): """Return a dict containing changes between two snapshots.""" changes = {} for path in set(prev_snapshot.keys()) | set(next_snapshot.keys()): if path in prev_snapshot and path not in next_snapshot: changes[path] = 'Deleted' elif path not in prev_snapshot and path in next_snapshot: changes[path] = 'Created' else: prev_mtime = prev_snapshot[path] next_mtime = next_snapshot[path] if next_mtime > prev_mtime: changes[path] = 'Changed' return changes qtsass-0.4.0/requirements/000077500000000000000000000000001441041562100155525ustar00rootroot00000000000000qtsass-0.4.0/requirements/dev.txt000066400000000000000000000001751441041562100170740ustar00rootroot00000000000000codecov flaky isort==4.3.15 pycodestyle==2.5.0 pydocstyle==3.0.0 PySide2; python_version=="3.7" pytest pytest-cov yapf==0.26 qtsass-0.4.0/requirements/install.txt000066400000000000000000000000101441041562100177500ustar00rootroot00000000000000libsass qtsass-0.4.0/requirements/release.txt000066400000000000000000000000231441041562100177260ustar00rootroot00000000000000loghub twine wheel qtsass-0.4.0/rever.xsh000066400000000000000000000204341441041562100147010ustar00rootroot00000000000000# Standard library imports import ast import os # Third party imports from rever.activity import activity from rever.tools import replace_in_file $ACTIVITIES = [ 'checkout', 'clean_repo', 'update_repo', 'install_deps', 'format_code', 'run_tests', 'update_release_version', 'create_python_distributions', 'upload_test_distributions', 'install_test_distributions', 'run_install_tests', 'create_changelog', 'commit_release_version', 'authors', 'add_tag', 'upload_python_distributions', 'update_dev_version', 'commit_dev_version', 'push', ] $PROJECT = "qtsass" $MODULE = $PROJECT $GITHUB_ORG = 'spyder-ide' $GITHUB_REPO = $PROJECT $VERSION_BUMP_PATTERNS = [ # These note where/how to find the version numbers ($MODULE + '/__init__.py', r'__version__\s*=.*', '__version__ = "$VERSION"'), ] $AUTHORS_FILENAME = "AUTHORS.md" $AUTHORS_TEMPLATE = """# Authors The $PROJECT project has some great contributors! They are: {authors} These have been sorted {sorting_text}. """ $AUTHORS_FORMAT= "- [{name}](https://github.com/{github})\n" $AUTHORS_SORTBY = "alpha" $TEMP_ENV = 'tmp-' + $PROJECT $CONDA_ACTIVATE_SCRIPT = 'activate.xsh' $HERE = os.path.abspath(os.path.dirname(__file__)) # --- Helpers # ---------------------------------------------------------------------------- class Colors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' def cprint(text, color): """Print colored text.""" print(color + text + Colors.ENDC) def get_version(version_type, module=$MODULE): """ Get version info. Tuple with three items, major.minor.patch """ with open(os.path.join($HERE, module, "__init__.py")) as fh: data = fh.read() major, minor, patch = 'MAJOR', 'MINOR', 'PATCH' lines = data.split("\n") for line in lines: if line.startswith("__version__"): version = ast.literal_eval(line.split("=")[-1].strip()) major, minor, patch = [int(v) for v in version.split('.')[:3]] version_type = version_type.lower() if version_type == 'major': major += 1 minor = 0 patch = 0 elif version_type == 'minor': minor += 1 patch = 0 elif version_type == 'patch': patch += 1 elif version_type in ['check', 'setup']: pass elif len(version_type.split('.')) == 3: major, minor, patch = version_type.split('.') else: raise Exception('Invalid option! Must provide version type: [major|minor|patch|MAJOR.MINOR.PATCH]') major = str(major) minor = str(minor) patch = str(patch) version = '.'.join([major, minor, patch]) if version_type not in ['check', 'setup']: cprint('\n\nReleasing version {}'.format(version), Colors.OKBLUE) print('\n\n') return version # Actual versions to use $NEW_VERSION = get_version($VERSION) $DEV_VERSION = $NEW_VERSION + '.dev0' def activate(env_name): """ Activate a conda environment. """ if not os.path.isfile($CONDA_ACTIVATE_SCRIPT): with open('activate.xsh', 'w') as fh: fh.write($(conda shell.xonsh hook)) # Activate environment source activate.xsh conda activate @(env_name) $[conda info] def update_version(version): """ Update version patterns. """ for fpath, pattern, new_pattern in $VERSION_BUMP_PATTERNS: new_pattern = new_pattern.replace('$VERSION', version) replace_in_file(pattern, new_pattern, fpath) # --- Activities # ---------------------------------------------------------------------------- @activity def checkout(branch='master'): """ Checkout master branch. """ git stash git checkout @(branch) # Check that origin and remote exist remotes = $(git remote -v) if not ('origin' in remotes and 'upstream' in remotes): raise Exception('Must have git remotes origin (pointing to fork) and upstream (pointing to repo)') @activity def clean_repo(): """ Clean the repo from build/dist and other files. """ import pathlib # Remove python files for p in pathlib.Path('.').rglob('*.py[co]'): p.unlink() for p in pathlib.Path('.').rglob('*.orig'): p.unlink() for p in pathlib.Path('.').rglob('__pycache__'): p.rmdir() rm -rf CHANGELOG.temp rm -rf .pytest_cache/ rm -rf build/ rm -rf dist/ rm -rf activate.xsh rm -rf $MODULE.egg-info # Delete files not tracked by git? # git clean -xfd @activity def update_repo(branch='master'): """ Stash any current changes and ensure you have the latest version from origin. """ git stash git pull upstream @(branch) git push origin @(branch) @activity def install_deps(): """ Install release and test dependencies. """ try: conda remove --name $TEMP_ENV --yes --quiet --all except: pass conda create --name $TEMP_ENV python=3.7 --yes --quiet activate($TEMP_ENV) pip install -r requirements/install.txt pip install -r requirements/dev.txt pip install -r requirements/release.txt @activity def format_code(): """ Format code. """ activate($TEMP_ENV) try: python run_checks_and_format.py except Exception: pass @activity def run_tests(): """ Run simple import tests before cleaning repository. """ pytest tests --cov=$MODULE @activity def update_release_version(): """ Update version in `__init__.py` (set release version, remove 'dev0'). and on the package.json file. """ update_version($NEW_VERSION) @activity def create_python_distributions(): """ Create distributions. """ activate($TEMP_ENV) python setup.py sdist bdist_wheel @activity def upload_test_distributions(): """ Upload test distributions. """ activate($TEMP_ENV) cprint("Yow will be asked to provide credentials", Colors.OKBLUE) print("\n\n") # The file might be already there try: python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* except Exception as err: print(err) @activity def install_test_distributions(): """ Upload test distributions. """ activate($TEMP_ENV) # Python package pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple $PROJECT==$NEW_VERSION @activity def run_install_tests(): """ Run simple import tests before cleaning repository. """ activate($TEMP_ENV) $MODULE --help @activity def create_changelog(): """ Create changelog using loghub. """ loghub $GITHUB_ORG/$GITHUB_REPO -zr @($PROJECT + ' v' + $NEW_VERSION) with open('CHANGELOG.temp', 'r') as fh: new_changelog_lines = fh.read().split('\n') with open('CHANGELOG.md', 'r') as fh: lines = fh.read().split('\n') new_lines = lines[:2] + new_changelog_lines + lines[2:] with open('CHANGELOG.md', 'w') as fh: fh.write('\n'.join(new_lines)) @activity def commit_release_version(): """ Commit release version. """ git add . git commit -m @('Set release version to ' + $NEW_VERSION + ' [ci skip]') @activity def add_tag(): """ Add release tag. """ # TODO: Add check to see if tag already exists? git tag -a @('v' + $NEW_VERSION) -m @('Tag version ' + $NEW_VERSION + ' [ci skip]') @activity def upload_python_distributions(): """ Upload the distributions to pypi production environment. """ activate($TEMP_ENV) cprint("Yow will be asked to provide credentials", Colors.OKBLUE) print("\n\n") # The file might be already there try: twine upload dist/* except Exception as err: print(err) @activity def update_dev_version(): """ Update `__init__.py` (add 'dev0'). """ update_version($DEV_VERSION) @activity def commit_dev_version(): """" Commit dev changes. """ git add . git commit -m "Restore dev version [ci skip]" --no-verify @activity def push(branch='master'): """ Push changes. """ # Push changes git push origin @(branch) git push upstream @(branch) # Push tags git push origin --tags git push upstream --tags qtsass-0.4.0/run_checks_and_format.py000066400000000000000000000033541441041562100177240ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Run checks and format code.""" # yapf: disable # Standard library imports from subprocess import PIPE, Popen import sys # yapf: enable # Constants COMMANDS = [ ['pydocstyle', 'qtsass'], ['pycodestyle', 'qtsass'], ['yapf', 'qtsass', '--in-place', '--recursive'], ['isort', '-y'], ] def run_process(cmd_list): """Run popen process.""" try: p = Popen(cmd_list, stdout=PIPE, stderr=PIPE) except OSError: raise OSError('Could not call command list: "%s"' % cmd_list) out, err = p.communicate() out = out.decode() err = err.decode() return out, err def repo_changes(): """Check if repo files changed.""" out, _err = run_process(['git', 'status', '--short']) out_lines = [l for l in out.split('\n') if l.strip()] return out_lines def run(): """Run linters and formatters.""" for cmd_list in COMMANDS: cmd_str = ' '.join(cmd_list) print('\nRunning: ' + cmd_str) out, err = run_process(cmd_list) if out: print(out) if err: print(err) out_lines = repo_changes() if out_lines: print('\nPlease run the linter and formatter script!') print('\n'.join(out_lines)) code = 1 else: print('\nAll checks passed!') code = 0 print('\n') sys.exit(code) if __name__ == '__main__': run() qtsass-0.4.0/setup.cfg000066400000000000000000000023111441041562100146450ustar00rootroot00000000000000[metadata] # This includes the license file(s) in the wheel. # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file license_files = LICENSE.txt # pydocstyle # http://www.pydocstyle.org/en/latest/usage.html # [pydocstyle] # pycodestyle # http://pycodestyle.pycqa.org/en/latest/intro.html#configuration [pycodestyle] max-line-length = 79 statistics = True # yapf # https://github.com/google/yapf#formatting-style [yapf:style] based_on_style = pep8 column_limit = 79 spaces_before_comment = 2 allow_multiline_lambdas = true dedent_closing_brackets = true blank_line_before_nested_class_or_def = false # isort # https://github.com/timothycrosley/isort/wiki/isort-Settings [isort] from_first = true import_heading_stdlib = Standard library imports import_heading_firstparty = Local imports import_heading_localfolder = Local imports import_heading_thirdparty = Third party imports indent = ' ' known_first_party = qtsass known_third_party = libsass,pytest,setuptools,watchdog default_section = THIRDPARTY line_length = 79 sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER lines_after_imports = 2 skip = venv multi_line_output = 3 include_trailing_comma = true qtsass-0.4.0/setup.py000066400000000000000000000047301441041562100145450ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Setup script for qtsass.""" # Standard library imports from io import open import ast import os # Third party imports from setuptools import find_packages, setup # Constants HERE = os.path.abspath(os.path.dirname(__file__)) def get_version(module='qtsass'): """Get version.""" with open(os.path.join(HERE, module, '__init__.py'), 'r') as f: data = f.read() lines = data.split('\n') for line in lines: if line.startswith('__version__'): version = ast.literal_eval(line.split('=')[-1].strip()) break return version def get_description(): """Get long description.""" with open(os.path.join(HERE, 'README.md'), 'r', encoding='utf-8') as f: data = f.read() return data setup( name='qtsass', version=get_version(), description='Compile SCSS files to valid Qt stylesheets.', long_description=get_description(), long_description_content_type='text/markdown', author='Yann Lanthony', maintainer='The Spyder Project Contributors', maintainer_email='qtsass@spyder-ide.org', url='https://github.com/spyder-ide/qtsass', license='MIT', packages=find_packages(exclude=['contrib', 'docs', 'tests*']), entry_points={ 'console_scripts': [ 'qtsass = qtsass.cli:main' ] }, classifiers=( 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Topic :: Software Development :: Build Tools', 'Topic :: Software Development :: Libraries :: Python Modules', ), install_requires=[ 'libsass>=0.22.0', ], python_requires='>=3.7', keywords='qt sass qtsass scss css qss stylesheets', ) qtsass-0.4.0/tests/000077500000000000000000000000001441041562100141715ustar00rootroot00000000000000qtsass-0.4.0/tests/__init__.py000066400000000000000000000022031441041562100162770ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- # Standard library imports from os.path import dirname, join, normpath import os import time PROJECT_DIR = normpath(dirname(dirname(__file__))) EXAMPLES_DIR = normpath(join(PROJECT_DIR, 'examples')) def example(*paths): """Get path to an example.""" return normpath(join(dirname(__file__), '..', 'examples', *paths)) def touch(file): """Touch a file.""" with open(str(file), 'a'): os.utime(str(file), None) def await_condition(condition, timeout=20, qt_app=None): """Return True if a condition is met in the given timeout period""" for _ in range(timeout): if qt_app: # pump event loop while waiting for condition qt_app.processEvents() if condition(): return True time.sleep(0.1) return False qtsass-0.4.0/tests/test_api.py000066400000000000000000000102501441041562100163510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Test qtsass api.""" from __future__ import absolute_import # Standard library imports from os.path import exists import logging # Third party imports import pytest import sass # Local imports import qtsass # Local imports from . import EXAMPLES_DIR, PROJECT_DIR, example COLORS_STR = """ QWidget { background: rgba(127, 127, 127, 100%); color: rgb(255, 255, 255); } """ QLINEARGRADIENTS_STR = """ QWidget { background: qlineargradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0.1 blue, stop: 0.8 green ); } """ QRADIANTGRADIENTS_STR = """ QWidget { background: qradialgradient( spread: repeat, cx: 0, cy: 0, fx: 0, fy: 1, stop: 0.1 blue, stop: 0.8 green ); } """ QNOT_STR = """ QLineEdit:!editable { background: white; } """ IMPORT_STR = """ @import 'dummy'; """ CUSTOM_BORDER_STR = """ QWidget { border: custom_border(); } """ def setup_module(): qtsass.enable_logging(level=logging.DEBUG) def teardown_module(): qtsass.enable_logging(level=logging.WARNING) def test_compile_strings(): """compile various strings.""" qtsass.compile(COLORS_STR) qtsass.compile(QLINEARGRADIENTS_STR) qtsass.compile(QRADIANTGRADIENTS_STR) qtsass.compile(QNOT_STR) def test_compile_import_raises(): """compile string with import raises.""" with pytest.raises(sass.CompileError): qtsass.compile(IMPORT_STR) def test_compile_import_with_include_paths(): """compile string with include_paths""" qtsass.compile(IMPORT_STR, include_paths=[EXAMPLES_DIR]) def test_compile_raises_ValueError(): """compile raises ValueError with invalid arguments""" # Pass invalid type to importers - must be sequence with pytest.raises(ValueError): qtsass.compile(COLORS_STR, importers=lambda x: None) # Pass invalid type to custom_functions with pytest.raises(ValueError): qtsass.compile(COLORS_STR, custom_functions=lambda x: None) def test_compile_custom_function(): """compile string with custom_functions""" custom_str = ( 'QWidget {\n' ' border: custom_border();\n' '}' ) def custom_border(): return '1px solid' css = qtsass.compile(custom_str, custom_functions=[custom_border]) assert '1px solid' in css assert 'custom_border()' not in css def test_compile_filename(tmpdir): """compile_filename simple.""" output = tmpdir.join('dummy.css') qtsass.compile_filename(example('dummy.scss'), output.strpath) assert exists(output.strpath) def test_compile_filename_no_save(): """compile_filename simple.""" qss = qtsass.compile_filename(example('dummy.scss')) assert isinstance(qss, str) def test_compile_filename_imports(tmpdir): """compile_filename with imports.""" output = tmpdir.join('dark.css') qtsass.compile_filename(example('complex', 'dark.scss'), output.strpath) assert exists(output.strpath) def test_compile_filename_imports_no_save(): """compile_filename with imports.""" qss = qtsass.compile_filename(example('complex', 'dark.scss')) assert isinstance(qss, str) def test_compile_dirname(tmpdir): """compile_dirname complex.""" output = tmpdir.join('complex') qtsass.compile_dirname(example('complex'), output.strpath) assert exists(output.join('dark.css').strpath) assert exists(output.join('light.css').strpath) def test_watch_raises_ValueError(tmpdir): """watch raises ValueError when source does not exist.""" # Watch file does not raise _ = qtsass.watch(example('dummy.scss'), tmpdir.join('dummy.scss').strpath) # Watch dir does not raise _ = qtsass.watch(example('complex'), tmpdir.join('complex').strpath) with pytest.raises(ValueError): _ = qtsass.watch('does_not_exist', 'does_not_exist') qtsass-0.4.0/tests/test_cli.py000066400000000000000000000144271441041562100163610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Test qtsass cli.""" from __future__ import absolute_import # Standard library imports from collections import namedtuple from os.path import basename, exists from subprocess import PIPE, Popen import time # Local imports from . import PROJECT_DIR, await_condition, example, touch SLEEP_INTERVAL = 1 Result = namedtuple('Result', "code stdout stderr") def indent(text, prefix=' '): """Like textwrap.indent""" return ''.join([prefix + line for line in text.splitlines(True)]) def invoke(args): """Invoke qtsass cli with specified args""" kwargs = dict( stdout=PIPE, stderr=PIPE, cwd=PROJECT_DIR ) proc = Popen(['python', '-m', 'qtsass'] + args, **kwargs) return proc def invoke_with_result(args): """Invoke qtsass cli and return a Result obj""" proc = invoke(args) out, err = proc.communicate() out = out.decode('ascii', errors="ignore") err = err.decode('ascii', errors="ignore") return Result(proc.returncode, out, err) def kill(proc, timeout=1): """Kill a subprocess and return a Result obj""" proc.kill() out, err = proc.communicate() out = out.decode('ascii', errors="ignore") err = err.decode('ascii', errors="ignore") return Result(proc.returncode, out, err) def format_result(result): """Format a subprocess Result obj""" out = [ 'Subprocess Report...', 'Exit code: %s' % result.code, ] if result.stdout: out.append('stdout:') out.append(indent(result.stdout, ' ')) if result.stderr: out.append('stderr:') out.append(indent(result.stderr, ' ')) return '\n'.join(out) def test_compile_dummy_to_stdout(): """CLI compile dummy example to stdout.""" args = [example('dummy.scss')] result = invoke_with_result(args) assert result.code == 0 assert result.stdout def test_compile_dummy_to_file(tmpdir): """CLI compile dummy example to file.""" input = example('dummy.scss') output = tmpdir.join('dummy.css') args = [input, '-o', output.strpath] result = invoke_with_result(args) assert result.code == 0 assert exists(output.strpath) def test_watch_dummy(tmpdir): """CLI watch dummy example.""" input = example('dummy.scss') output = tmpdir.join('dummy.css') args = [input, '-o', output.strpath, '-w'] proc = invoke(args) # Wait for initial compile output_exists = lambda: exists(output.strpath) if not await_condition(output_exists): result = kill(proc) report = format_result(result) err = "Failed to compile dummy.scss\n" err += report assert False, report # Ensure subprocess is still alive assert proc.poll() is None # Touch input file, triggering a recompile created = output.mtime() file_modified = lambda: output.mtime() > created time.sleep(SLEEP_INTERVAL) touch(input) if not await_condition(file_modified): result = kill(proc) report = format_result(result) err = 'Modifying %s did not trigger recompile.\n' % basename(input) err += report assert False, err kill(proc) def test_compile_complex(tmpdir): """CLI compile complex example.""" input = example('complex') output = tmpdir.mkdir('output') args = [input, '-o', output.strpath] result = invoke_with_result(args) assert result.code == 0 expected_files = [output.join('light.css'), output.join('dark.css')] for file in expected_files: assert exists(file.strpath) def test_watch_complex(tmpdir): """CLI watch complex example.""" input = example('complex') output = tmpdir.mkdir('output') args = [input, '-o', output.strpath, '-w'] proc = invoke(args) expected_files = [output.join('light.css'), output.join('dark.css')] # Wait for initial compile files_created = lambda: all([exists(f.strpath) for f in expected_files]) if not await_condition(files_created): result = kill(proc) report = format_result(result) err = 'All expected files have not been created...' err += report assert False, err # Ensure subprocess is still alive assert proc.poll() is None # Input files to touch input_full = example('complex', 'light.scss') input_partial = example('complex', '_base.scss') input_nested = example('complex', 'widgets', '_qwidget.scss') def touch_and_wait(input_file, timeout=2000): """Touch a file, triggering a recompile""" filename = basename(input_file) old_mtimes = [f.mtime() for f in expected_files] files_modified = lambda: all( [f.mtime() > old_mtimes[i] for i, f in enumerate(expected_files)] ) time.sleep(SLEEP_INTERVAL) touch(input_file) if not await_condition(files_modified, timeout): result = kill(proc) report = format_result(result) err = 'Modifying %s did not trigger recompile.\n' % filename err += report for i, f in enumerate(expected_files): err += str(f) + '\n' err += str(old_mtimes[i]) + '\n' err += str(f.mtime()) + '\n' err += str(bool(f.mtime() > old_mtimes[i])) + '\n' assert False, err return True assert touch_and_wait(input_full) assert touch_and_wait(input_partial) assert touch_and_wait(input_nested) kill(proc) def test_invalid_input(): """CLI input is not a file or dir.""" proc = invoke_with_result(['file_does_not_exist.scss']) assert proc.code == 1 assert 'Error: input must be' in proc.stdout proc = invoke_with_result(['./dir/does/not/exist']) assert proc.code == 1 assert 'Error: input must be' in proc.stdout def test_dir_missing_output(): """CLI dir missing output option""" proc = invoke_with_result([example('complex')]) assert proc.code == 1 assert 'Error: missing required option' in proc.stdout qtsass-0.4.0/tests/test_conformers.py000066400000000000000000000212411441041562100177570ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Test qtsass conformers.""" from __future__ import absolute_import # Standard library imports from textwrap import dedent import unittest # Local imports from qtsass.conformers import ( NotConformer, QLinearGradientConformer, QRadialGradientConformer, ) class TestNotConformer(unittest.TestCase): qss_str = 'QAbstractItemView::item:!active' css_str = 'QAbstractItemView::item:_qnot_active' def test_conform_to_scss(self): """NotConformer qss to scss.""" c = NotConformer() self.assertEqual(c.to_scss(self.qss_str), self.css_str) def test_conform_to_qss(self): """NotConformer css to qss.""" c = NotConformer() self.assertEqual(c.to_qss(self.css_str), self.qss_str) def test_round_trip(self): """NotConformer roundtrip.""" c = NotConformer() conformed_css = c.to_scss(self.qss_str) self.assertEqual(c.to_qss(conformed_css), self.qss_str) class TestQLinearGradientConformer(unittest.TestCase): css_vars_str = 'qlineargradient($x1, $y1, $x2, $y2, (0 $red, 1 $blue))' qss_vars_str = ( 'qlineargradient(x1:$x1, x2:$x2, y1:$y1, y2:$y2' 'stop: 0 $red, stop: 1 $blue)' ) css_nostops_str = 'qlineargradient(0, 0, 0, 0)' qss_nostops_str = 'qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0)' css_str = 'qlineargradient(0, 0, 0, 0, (0 red, 1 blue))' qss_singleline_str = ( 'qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0, ' 'stop: 0 red, stop: 1 blue)' ) qss_multiline_str = dedent(""" qlineargradient( x1: 0, y1: 0, x2: 0, y2: 0, stop: 0 red, stop: 1 blue ) """).strip() qss_weird_whitespace_str = ( 'qlineargradient( x1: 0, y1:0, x2: 0, y2:0, ' ' stop:0 red, stop: 1 blue )' ) css_rgba_str = ( 'qlineargradient(0, 0, 0, 0, ' '(0 rgba(0, 1, 2, 30%), 0.99 rgba(7, 8, 9, 100%)))' ) qss_rgba_str = ( 'qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0, ' 'stop: 0 rgba(0, 1, 2, 30%), stop: 0.99 rgba(7, 8, 9, 100%))' ) css_incomplete_coords_str = ( 'qlineargradient(0, 1, 0, 0, (0 red, 1 blue))' ) qss_incomplete_coords_str = ( 'qlineargradient(y1:1, stop:0 red, stop: 1 blue)' ) css_float_coords_str = ( 'qlineargradient(0, 0.75, 0, 0, (0 green, 1 pink))' ) qss_float_coords_str = ( 'qlineargradient(y1:0.75, stop:0 green, stop: 1 pink)' ) def test_does_not_affect_css_form(self): """QLinearGradientConformer no affect on css qlineargradient func.""" c = QLinearGradientConformer() self.assertEqual(c.to_scss(self.css_str), self.css_str) self.assertEqual(c.to_qss(self.css_str), self.css_str) def test_conform_singleline_str(self): """QLinearGradientConformer singleline qss to scss.""" c = QLinearGradientConformer() self.assertEqual(c.to_scss(self.qss_singleline_str), self.css_str) def test_conform_multiline_str(self): """QLinearGradientConformer multiline qss to scss.""" c = QLinearGradientConformer() self.assertEqual(c.to_scss(self.qss_multiline_str), self.css_str) def test_conform_weird_whitespace_str(self): """QLinearGradientConformer weird whitespace qss to scss.""" c = QLinearGradientConformer() self.assertEqual(c.to_scss(self.qss_weird_whitespace_str), self.css_str) def test_conform_nostops_str(self): """QLinearGradientConformer qss with no stops to scss.""" c = QLinearGradientConformer() self.assertEqual(c.to_scss(self.qss_nostops_str), self.css_nostops_str) def test_conform_vars_str(self): """QLinearGradientConformer qss with vars to scss.""" c = QLinearGradientConformer() self.assertEqual(c.to_scss(self.qss_vars_str), self.css_vars_str) def test_conform_rgba_str(self): """QLinearGradientConformer qss with rgba to scss.""" c = QLinearGradientConformer() self.assertEqual(c.to_scss(self.qss_rgba_str), self.css_rgba_str) def test_incomplete_coords(self): """QLinearGradientConformer qss with not all 4 coordinates given.""" c = QLinearGradientConformer() self.assertEqual(c.to_scss(self.qss_incomplete_coords_str), self.css_incomplete_coords_str) def test_float_coords(self): c = QLinearGradientConformer() self.assertEqual(c.to_scss(self.qss_float_coords_str), self.css_float_coords_str) class TestQRadialGradientConformer(unittest.TestCase): css_vars_str = "qradialgradient('$spread', $cx, $cy, $radius, $fx, $fy, (0 $red, 1 $blue))" qss_vars_str = ( 'qradialgradient(spread:$spread, cx:$cx, cy:$cy, radius:$radius, fx:$fx, fy:$fy,' 'stop: 0 $red, stop: 1 $blue)' ) css_nostops_str = "qradialgradient('pad', 0, 0, 0, 0, 0)" qss_nostops_str = 'qradialgradient(spread: pad, cx: 0, cy: 0, fx: 0, fy: 0)' css_str = "qradialgradient('pad', 0, 0, 0, 0, 0, (0 red, 1 blue))" qss_singleline_str = ( 'qradialgradient(spread: pad, cx: 0, cy: 0, fx: 0, fy: 0, ' 'stop: 0 red, stop: 1 blue)' ) qss_multiline_str = dedent(""" qradialgradient( spread: pad, cx: 0, cy: 0, fx: 0, fy: 0, stop: 0 red, stop: 1 blue ) """).strip() qss_weird_whitespace_str = ( 'qradialgradient( spread: pad, cx: 0, cy:0, fx: 0, fy:0, ' ' stop:0 red, stop: 1 blue )' ) css_rgba_str = ( "qradialgradient('pad', 0, 0, 0, 0, 0, " "(0 rgba(0, 1, 2, 30%), 0.99 rgba(7, 8, 9, 100%)))" ) qss_rgba_str = ( 'qradialgradient(spread: pad, cx: 0, cy: 0, fx: 0, fy: 0, ' 'stop: 0 rgba(0, 1, 2, 30%), stop: 0.99 rgba(7, 8, 9, 100%))' ) css_incomplete_coords_str = ( "qradialgradient('pad', 0, 1, 0, 0, 0, (0 red, 1 blue))" ) qss_incomplete_coords_str = ( 'qradialgradient(spread:pad, cy:1, stop:0 red, stop: 1 blue)' ) css_float_coords_str = ( "qradialgradient('pad', 0, 0.75, 0, 0, 0, (0 green, 1 pink))" ) qss_float_coords_str = ( 'qradialgradient(spread: pad, cy:0.75, stop:0 green, stop: 1 pink)' ) def test_does_not_affect_css_form(self): """QRadialGradientConformer no affect on css qradialgradient func.""" c = QRadialGradientConformer() self.assertEqual(c.to_scss(self.css_str), self.css_str) self.assertEqual(c.to_qss(self.css_str), self.css_str) def test_conform_singleline_str(self): """QRadialGradientConformer singleline qss to scss.""" c = QRadialGradientConformer() self.assertEqual(c.to_scss(self.qss_singleline_str), self.css_str) def test_conform_multiline_str(self): """QRadialGradientConformer multiline qss to scss.""" c = QRadialGradientConformer() self.assertEqual(c.to_scss(self.qss_multiline_str), self.css_str) def test_conform_weird_whitespace_str(self): """QRadialGradientConformer weird whitespace qss to scss.""" c = QRadialGradientConformer() self.assertEqual(c.to_scss(self.qss_weird_whitespace_str), self.css_str) def test_conform_nostops_str(self): """QRadialGradientConformer qss with no stops to scss.""" c = QRadialGradientConformer() self.assertEqual(c.to_scss(self.qss_nostops_str), self.css_nostops_str) def test_conform_vars_str(self): """QRadialGradientConformer qss with vars to scss.""" c = QRadialGradientConformer() self.assertEqual(c.to_scss(self.qss_vars_str), self.css_vars_str) def test_conform_rgba_str(self): """QRadialGradientConformer qss with rgba to scss.""" c = QRadialGradientConformer() self.assertEqual(c.to_scss(self.qss_rgba_str), self.css_rgba_str) def test_incomplete_coords(self): """QRadialGradientConformer qss with not all 4 coordinates given.""" c = QRadialGradientConformer() self.assertEqual(c.to_scss(self.qss_incomplete_coords_str), self.css_incomplete_coords_str) def test_float_coords(self): c = QRadialGradientConformer() self.assertEqual(c.to_scss(self.qss_float_coords_str), self.css_float_coords_str) if __name__ == "__main__": unittest.main(verbosity=2) qtsass-0.4.0/tests/test_functions.py000066400000000000000000000053621441041562100176200ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Test qtsass custom functions.""" from __future__ import absolute_import # Standard library imports import unittest # Local imports from qtsass.api import compile class BaseCompileTest(unittest.TestCase): def compile_scss(self, string): # NOTE: revise for better future compatibility wstr = '*{{t: {0};}}'.format(string) res = compile(wstr) return res.replace('* {\n t: ', '').replace('; }\n', '') class TestRgbaFunc(BaseCompileTest): def test_rgba(self): self.assertEqual( self.compile_scss('rgba(0, 1, 2, 0.3)'), 'rgba(0, 1, 2, 30%)' ) def test_rgba_percentage_alpha(self): result = self.compile_scss('rgba(255, 0, 125, 75%)') self.assertEqual(result, 'rgba(255, 0, 125, 75%)') def test_rgba_8bit_int_alpha(self): for in_val, out_val in ((0, 0), (128, 50), (255, 100)): result = self.compile_scss('rgba(255, 0, 125, %i)' % in_val) self.assertEqual(result, 'rgba(255, 0, 125, %i%%)' % out_val) class TestQLinearGradientFunc(BaseCompileTest): def test_color(self): self.assertEqual( self.compile_scss('qlineargradient(1, 2, 3, 4, (0 red, 1 blue))'), 'qlineargradient(x1: 1.0, y1: 2.0, x2: 3.0, y2: 4.0, ' 'stop: 0.0 rgba(255, 0, 0, 100%), stop: 1.0 rgba(0, 0, 255, 100%))' ) def test_rgba(self): self.assertEqual( self.compile_scss('qlineargradient(1, 2, 3, 4, (0 red, 0.2 rgba(5, 6, 7, 0.8)))'), 'qlineargradient(x1: 1.0, y1: 2.0, x2: 3.0, y2: 4.0, ' 'stop: 0.0 rgba(255, 0, 0, 100%), stop: 0.2 rgba(5, 6, 7, 80%))' ) class TestQRadialGradientFunc(BaseCompileTest): def test_color(self): self.assertEqual( self.compile_scss('qradialgradient(pad, 1, 2, 1, 3, 4, (0 red, 1 blue))'), 'qradialgradient(spread: pad, cx: 1.0, cy: 2.0, radius: 1.0, fx: 3.0, fy: 4.0, ' 'stop: 0.0 rgba(255, 0, 0, 100%), stop: 1.0 rgba(0, 0, 255, 100%))' ) def test_rgba(self): self.assertEqual( self.compile_scss('qradialgradient(pad, 1, 2, 1, 3, 4, (0 red, 0.2 rgba(5, 6, 7, 0.8)))'), 'qradialgradient(spread: pad, cx: 1.0, cy: 2.0, radius: 1.0, fx: 3.0, fy: 4.0, ' 'stop: 0.0 rgba(255, 0, 0, 100%), stop: 0.2 rgba(5, 6, 7, 80%))' ) if __name__ == "__main__": unittest.main(verbosity=2) qtsass-0.4.0/tests/test_watchers.py000066400000000000000000000101531441041562100174220ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2015 Yann Lanthony # Copyright (c) 2017-2018 Spyder Project Contributors # # Licensed under the terms of the MIT License # (See LICENSE.txt for details) # ----------------------------------------------------------------------------- """Test qtsass cli.""" from __future__ import absolute_import # Standard library imports from os.path import dirname, exists import os import shutil import sys import time # Third party imports from flaky import flaky import pytest # Local imports #Local imports from qtsass import compile_filename from qtsass.watchers import PollingWatcher, QtWatcher from qtsass.watchers.api import retry # Local imports from . import EXAMPLES_DIR, await_condition, example, touch class CallCounter(object): def __init__(self): self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 @pytest.mark.parametrize( 'Watcher', (PollingWatcher, QtWatcher), ) @flaky(max_runs=3) def test_watchers(Watcher, tmpdir): """Stress test Watcher implementations""" # Skip when QtWatcher is None - when Qt is not installed. if not Watcher: return watch_dir = tmpdir.join('src').strpath os.makedirs(watch_dir) shutil.copy2(example('dummy.scss'), watch_dir) input = tmpdir.join('src/dummy.scss').strpath output = tmpdir.join('build/dummy.css').strpath output_exists = lambda: exists(output) c = CallCounter() w = Watcher( watch_dir=watch_dir, compiler=compile_filename, args=(input, output), ) w.connect(c) # Output should not yet exist assert not exists(output) w.start() touch(input) time.sleep(0.5) if not await_condition(output_exists): assert False, 'Output file not created...' # Removing the watch_dir should not kill the Watcher # simply stop dispatching callbacks shutil.rmtree(watch_dir) time.sleep(0.5) assert c.count == 1 # Watcher should recover once the input file is there again os.makedirs(watch_dir) shutil.copy2(example('dummy.scss'), watch_dir) time.sleep(0.5) assert c.count == 2 # Stop watcher w.stop() w.join() for _ in range(5): touch(input) # Count should not change assert c.count == 2 @pytest.mark.skipif(sys.platform.startswith('linux') or not QtWatcher, reason="Fails on linux") def test_qtwatcher(tmpdir): """Test QtWatcher implementation.""" # Constructing a QApplication will cause the QtWatcher constructed # below to use a Signal to dispatch callbacks. from qtsass.watchers.qt import QApplication qt_app = QApplication.instance() if not qt_app: qt_app = QApplication([]) watch_dir = tmpdir.join('src').strpath os.makedirs(watch_dir) shutil.copy2(example('dummy.scss'), watch_dir) input = tmpdir.join('src/dummy.scss').strpath output = tmpdir.join('build/dummy.css').strpath output_exists = lambda: exists(output) c = CallCounter() w = QtWatcher( watch_dir=watch_dir, compiler=compile_filename, args=(input, output), ) # We connect a counter directly to the Watcher's Qt Signal in order to # verify that the Watcher is actually using a Qt Signal. w.qtdispatcher.signal.connect(c) w.start() touch(input) time.sleep(0.5) if not await_condition(output_exists, qt_app=qt_app): assert False, 'Output file not created...' assert c.count == 1 # Stop watcher w.stop() w.join() def test_retry(): """Test retry decorator""" @retry(5, interval=0) def succeeds_after(n, counter): counter() if n <= counter.count: return True raise ValueError # Succeed when attempts < retries assert succeeds_after(4, CallCounter()) # Fails when retries < attemps with pytest.raises(ValueError): assert succeeds_after(6, CallCounter()) @retry(5, interval=0) def fails(): raise ValueError # Most obvious case with pytest.raises(ValueError): fails()