├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── learnregex ├── __init__.py ├── alternation │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── anchors │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── capturing │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── character_classes │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── greediness │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── groups │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── introduction │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ └── pyschool.po │ └── es │ │ └── LC_MESSAGES │ │ └── pyschool.po ├── negated_character_classes │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── quantifiers │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── special_characters │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py ├── the_dot │ ├── README.rst │ ├── SOLUTION.rst │ └── __init__.py └── utils.py ├── requirements_dev.pip ├── setup.cfg ├── setup.py ├── tests └── test_story.py ├── tox.ini └── travis_pypi_setup.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # pyenv python configuration file 62 | .python-version 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated and will overwrite each time you run travis_pypi_setup.py 2 | deploy: 3 | skip_cleanup: true 4 | skip_upload_docs: true 5 | true: 6 | condition: $TOXENV == py33 7 | repo: sophilabs/learnregex 8 | tags: true 9 | password: 10 | secure: !!binary | 11 | cjQ4dU9TcGt6N0UxY1c1WnpZRHk0bEVkTWI2eFlhMXh0cXJMTWs0bFFmY0hyRXMzb3MvNUZWbWlq 12 | TjFjTFM4QkJSNVpGb25vWDZhU2oxRXVRYmJYMm5oTm9HMEhiMnVJREw4NitENFNFbHdRTDJwWFRi 13 | KytBRUlmdTQ2SmN1UklkREl0U1VHZi9udkZDM0RpUzNvLy9HaGhqRmQ2Yk5WaVdEQzBqNUlMeENz 14 | ZjNkWjQrYThLam5MRGN1SVZTVjJxd0dvQVEwV1lOSmJEN2x6S2xCdWN6SG1xVGd6R3JGdnR2UHBu 15 | K0ZCSC9tbnVLQVlXT2xBaWVBQ1gyWTlRQjdkZ2QvVzFvZW16VytSM3Y4NW9nb01FU0lVa2dLMHN4 16 | ZFUzVXZsRG5TeTRUOEh3MGVuZGZMWmxMbkQ5M1NIeXZhSHplMlJ1Ry9rUC9tRDBaK0FIZWwyeWZs 17 | QStJT1RmS3BnYWlONElwZjJYZFU2TllDS2VJQVJDdHNWaGJQTDVqbFF1dzdGbU0yT1JvZUZlY0tO 18 | MzBCWkdDRjl4bjlWVG1aQitoWUhSeDFhdHFKU0NCdXVENXhibzQ2d1E4Q0VSOGZiL2dZVVJwcmRJ 19 | ZDF6OXFjOWpxM2NwMW9mRlpTaWNZTkJIV2V5R281Y2ZtNFlhbXRCSlo4UWZqVTdySE1URDFGRHF4 20 | NlNCc256VXlUZWRNdmN5V3YxMEt6VUVLeG1oR2xDb3lvVXRRQy9yWGROMXhEMUhOQzdNRzBLZG1O 21 | K1ZMa2twOXEwemd6dDQvSlpuSFVoYkkvM3h0bmtPZy83ZE4wS253YXZsNGx2WXNtdHduSzZVa1ZQ 22 | T2ZxeGNyY2lEWjhFQzJ3WEl3Yk5sY2xrRitrTGwwTnQwS1pZUVpiRkE2RXhNakVacXFUOGxYZlk9 23 | user: sophilabs 24 | provider: pypi 25 | distributions: sdist bdist_wheel 26 | env: 27 | - TOXENV=py35 28 | - TOXENV=py34 29 | - TOXENV=py33 30 | install: pip install -U tox 31 | language: python 32 | python: 3.5 33 | script: tox -e ${TOXENV} 34 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Javier Ayres 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Pablo Ricco 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every 8 | little bit helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/sophilabs/learnregex/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" 30 | and "help wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | learnregex could always use more documentation, whether as part of the 42 | official learnregex docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/sophilabs/learnregex/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `learnregex` for local development. 61 | 62 | 1. Fork the `learnregex` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/learnregex.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv learnregex 70 | $ cd learnregex/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 80 | 81 | $ flake8 learnregex tests 82 | $ python setup.py test or py.test 83 | $ tox 84 | 85 | To get flake8 and tox, just pip install them into your virtualenv. 86 | 87 | 6. Commit your changes and push your branch to GitHub:: 88 | 89 | $ git add . 90 | $ git commit -m "Your detailed description of your changes." 91 | $ git push origin name-of-your-bugfix-or-feature 92 | 93 | 7. Submit a pull request through the GitHub website. 94 | 95 | Pull Request Guidelines 96 | ----------------------- 97 | 98 | Before you submit a pull request, check that it meets these guidelines: 99 | 100 | 1. The pull request should include tests. 101 | 2. If the pull request adds functionality, the docs should be updated. Put 102 | your new functionality into a function with a docstring, and add the 103 | feature to the list in README.rst. 104 | 3. The pull request should work for Python 2.6, 2.7, 3.3, 3.4 and 3.5, and for PyPy. Check 105 | https://travis-ci.org/sophilabs/learnregex/pull_requests 106 | and make sure that the tests pass for all supported Python versions. 107 | 108 | Tips 109 | ---- 110 | 111 | To run a subset of tests:: 112 | 113 | 114 | $ python -m unittest tests.test_learnregex 115 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.1.0 (2017-04-05) 6 | ------------------ 7 | 8 | * First release on PyPI. 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Sophilabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include AUTHORS.rst 3 | 4 | include CONTRIBUTING.rst 5 | include HISTORY.rst 6 | include LICENSE 7 | include README.rst 8 | 9 | recursive-include tests * 10 | recursive-include learnregex *.py *.rst *.mo *.po *.pot 11 | 12 | recursive-exclude * __pycache__ 13 | recursive-exclude * *.py[co] 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | 25 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 26 | VERSION=$(shell grep __version__ learnregex/__init__.py) 27 | REQUIREMENTS="requirements_dev.pip" 28 | TAG="\n\n\033[0;32m\#\#\# " 29 | END=" \#\#\# \033[0m\n" 30 | 31 | all: test 32 | 33 | help: 34 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 35 | 36 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 37 | 38 | clean-build: 39 | @echo $(TAG)Remove build artifacts$(END) 40 | rm -fr build/ 41 | rm -fr dist/ 42 | rm -fr .eggs/ 43 | find . -name '*.egg-info' -exec rm -fr {} + 44 | find . -name '*.egg' -exec rm -f {} + 45 | @echo 46 | 47 | clean-pyc: 48 | @echo $(TAG)Remove Python file artifacts$(END) 49 | find . -name '*.pyc' -exec rm -f {} + 50 | find . -name '*.pyo' -exec rm -f {} + 51 | find . -name '*~' -exec rm -f {} + 52 | find . -name '__pycache__' -exec rm -fr {} + 53 | @echo 54 | 55 | clean-test: 56 | @echo $(TAG)Remove test and coverage artifacts$(END) 57 | rm -fr .tox/ 58 | rm -f .coverage 59 | rm -fr htmlcov/ 60 | @echo 61 | 62 | lint: 63 | @echo $(TAG)Remove test and coverage artifacts$(END) 64 | flake8 learnregex tests 65 | @echo 66 | 67 | msg-init: 68 | @echo $(TAG)Initializing messages$(END) 69 | pybabel init -D pyschool -i learnregex/locale/learnregex.pot -d learnregex/locale -l en 70 | pybabel init -D pyschool -i learnregex/locale/learnregex.pot -d learnregex/locale -l es 71 | @echo 72 | 73 | msg-extract: 74 | @echo $(TAG)Extracting messages$(END) 75 | pybabel extract -o learnregex/locale/learnregex.pot learnregex 76 | pybabel update -D pyschool -i learnregex/locale/learnregex.pot -d learnregex/locale -l en 77 | pybabel update -D pyschool -i learnregex/locale/learnregex.pot -d learnregex/locale -l es 78 | @echo 79 | 80 | msg-compile: 81 | @echo $(TAG)Compiling messages$(END) 82 | pybabel compile -D pyschool -d learnregex/locale -f --statistics 83 | @echo 84 | 85 | msg: msg-extract msg-compile 86 | 87 | test: 88 | @echo $(TAG)Run tests quickly with the default Python$(END) 89 | PYTHONPATH=. py.test ./tests 90 | @echo 91 | 92 | test-all: 93 | @echo $(TAG)Run tests on every Python version with tox$(END) 94 | tox 95 | @echo 96 | 97 | coverage: 98 | @echo $(TAG)Check code coverage quickly with the default Python$(END) 99 | coverage run --source learnregex -m pytest 100 | 101 | coverage report -m 102 | coverage html 103 | $(BROWSER) htmlcov/index.html 104 | @echo 105 | 106 | release: clean msg-compile 107 | @echo $(TAG)Package and upload a release$(END) 108 | python setup.py sdist upload 109 | python setup.py bdist_wheel upload 110 | @echo 111 | 112 | dist: clean msg-compile 113 | @echo $(TAG)Builds source and wheel package$(END) 114 | python setup.py sdist 115 | python setup.py bdist_wheel 116 | ls -l dist 117 | @echo 118 | 119 | install: clean 120 | @echo $(TAG)Install the package to the active Python site-packages$(END) 121 | python setup.py install 122 | @echo 123 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | learnregex 3 | =============================== 4 | 5 | 6 | .. image:: https://img.shields.io/pypi/v/learnregex.svg 7 | :target: https://pypi.python.org/pypi/learnregex 8 | 9 | .. image:: https://img.shields.io/travis/sophilabs/learnregex.svg 10 | :target: https://travis-ci.org/sophilabs/learnregex 11 | 12 | .. image:: https://pyup.io/repos/github/sophilabs/learnregex/shield.svg 13 | :target: https://pyup.io/repos/github/sophilabs/learnregex/ 14 | :alt: Updates 15 | 16 | 17 | A pyschool story for learning regular expressions. 18 | 19 | Credits 20 | ------- 21 | 22 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. 23 | 24 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 25 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 26 | 27 | License 28 | ------- 29 | 30 | learnregex is Copyright (c) 2016-2017 sophilabs, inc. It is free software, and may be 31 | redistributed under the terms specified in the `license <./LICENSE>`__ file. 32 | 33 | About 34 | ----- 35 | 36 | .. image:: https://s3.amazonaws.com/sophilabs-assets/logo/logo_300x66.gif 37 | :target: https://sophilabs.co 38 | 39 | learnregex is maintained and funded by sophilabs, inc. The names and logos for 40 | sophilabs are trademarks of sophilabs, inc. 41 | -------------------------------------------------------------------------------- /learnregex/__init__.py: -------------------------------------------------------------------------------- 1 | from story.story import BaseStory 2 | from story.translation import gettext as _ 3 | 4 | from . import (alternation, anchors, capturing, character_classes, greediness, 5 | groups, introduction, negated_character_classes, quantifiers, 6 | special_characters, the_dot) 7 | 8 | 9 | __author__ = """Sophilabs""" 10 | __email__ = 'hi@sophilabs.co' 11 | __version__ = '0.4.11' 12 | 13 | 14 | class Story(BaseStory): 15 | 16 | name = 'learnregex' 17 | title = _('Learn regular expressions with Python') 18 | adventures = [ 19 | introduction, 20 | special_characters, 21 | character_classes, 22 | negated_character_classes, 23 | the_dot, 24 | quantifiers, 25 | greediness, 26 | anchors, 27 | alternation, 28 | groups, 29 | capturing, 30 | ] 31 | -------------------------------------------------------------------------------- /learnregex/alternation/README.rst: -------------------------------------------------------------------------------- 1 | Alternation 2 | 3 | If you want to match different regular expressions on the same string, you 4 | can use the alternation operator (the pipe symbol |) to separate different 5 | expressions and instruct the engine to try to match either what's to the left 6 | of it or, if it fails, what's to the right of it. 7 | 8 | The alternation operator has the lowest precedence of all operators, meaning 9 | that the engine will try to match everything to its left as a whole and 10 | everything to its right (assuming the previous match failed) as a whole. If 11 | you want to limit the scope of the operator to use it inside a tiny part of a 12 | more complex expression you will need to learn how "groups" work. Luckily for 13 | you, that's the next adventure. 14 | 15 | For this adventure, write a python function that receives a string and 16 | returns `True` if it's either 'red', 'green' or 'blue'; and `False` otherwise. 17 | 18 | You can use this template to get started: 19 | 20 | .. sourcecode:: python 21 | 22 | import re 23 | 24 | def test(string): 25 | # Your code goes here 26 | 27 | HINT 28 | ---- 29 | You can use multiple alternation operators, they resolve from left to 30 | right. That means that the expression 'aa|bb|cc' will try to match 'aa' 31 | first, if it fails it will follow with 'bb|cc' where it will again split 32 | the expressions between the alternation operator and start with 'bb', and 33 | so on... 34 | 35 | When you are done, you must run: 36 | 37 | .. sourcecode:: bash 38 | 39 | $ {script} verify program.py 40 | -------------------------------------------------------------------------------- /learnregex/alternation/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.match(r'red$|green$|blue$', string) 10 | 11 | -------------------------------------------------------------------------------- /learnregex/alternation/__init__.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from story.adventures import AdventureVerificationError, BaseAdventure 4 | from story.translation import gettext as _ 5 | 6 | from ..utils import load_solution_function 7 | 8 | 9 | class Adventure(BaseAdventure): 10 | 11 | title = _('Alternation') 12 | choices = ['red', 'green', 'blue'] 13 | 14 | def test(self, file): 15 | function = load_solution_function(file) 16 | correct_argument = random.choice(self.choices) 17 | if not function(correct_argument): 18 | raise AdventureVerificationError( 19 | _("Your function didn't return True when executed with a " 20 | "correct argument '{}'.".format(correct_argument)) 21 | ) 22 | wrong_argument = (random.choice(self.choices) 23 | + random.choice(self.choices)) 24 | if function(wrong_argument): 25 | raise AdventureVerificationError( 26 | _("Your function returned True when executed with a wrong " 27 | "argument '{}'.".format(wrong_argument))) 28 | -------------------------------------------------------------------------------- /learnregex/anchors/README.rst: -------------------------------------------------------------------------------- 1 | Anchors allow us to match specific positions inside a string instead of a 2 | character. They belong to a group called zero-length matches for this reason. 3 | 4 | With anchors we can match the start and end of a string using ^ and $ 5 | respectively. 6 | 7 | For this adventure, write a python function that receives a string and 8 | returns `True` if it ends with a caret, and `False` otherwise. 9 | 10 | You can use this template to get started: 11 | 12 | .. sourcecode:: python 13 | 14 | import re 15 | 16 | def test(string): 17 | # Your code goes here 18 | 19 | When you are done, you must run: 20 | 21 | .. sourcecode:: bash 22 | 23 | $ {script} verify program.py 24 | -------------------------------------------------------------------------------- /learnregex/anchors/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.match(r'.*\^$', string) 10 | -------------------------------------------------------------------------------- /learnregex/anchors/__init__.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from story.adventures import AdventureVerificationError, BaseAdventure 4 | from story.translation import gettext as _ 5 | 6 | from ..utils import get_random_string, load_solution_function 7 | 8 | 9 | class Adventure(BaseAdventure): 10 | 11 | title = _('Anchors') 12 | dictionary = string.ascii_lowercase + string.digits 13 | 14 | def test(self, file): 15 | function = load_solution_function(file) 16 | correct_argument = get_random_string(self.dictionary, 4, 6) + '^' 17 | if not function(correct_argument): 18 | raise AdventureVerificationError( 19 | _("Your function didn't return True when executed with a " 20 | "correct argument '{}'.".format(correct_argument)) 21 | ) 22 | wrong_argument = get_random_string(self.dictionary, 4, 6) 23 | if function(wrong_argument): 24 | raise AdventureVerificationError( 25 | _("Your function returned True when executed with a wrong " 26 | "argument '{}'.".format(wrong_argument))) 27 | -------------------------------------------------------------------------------- /learnregex/capturing/README.rst: -------------------------------------------------------------------------------- 1 | Groups provide an additional feature: they capture the string they end up 2 | matching and save it in a variable that we can use later, either in the same 3 | regular expression or after it finishes. In python we can use a captured 4 | group with a backslash and an index. For example: \1 will reference the first 5 | group of our regular expression, \2 will reference the second one and so on 6 | until \9, where we run out of numbers (they can only use one digit). If you 7 | need to capture more than 9 groups check out named groups. 8 | 9 | Say we want to test if a string ends with the same character it started. We 10 | could write something like this: '^(.).*\1$|^.$'. We capture the first 11 | character after the start of the string, match zero o more characters in 12 | between and reference the captured character before the string ends. We also 13 | alternate with another expression in case our string has only 1 character. 14 | 15 | For this adventure, write a python function that receives a string with only 16 | one pipe ('|') somewhere in between and returns `True` if everything to the 17 | left of the pipe equals what's to its right, and `False` otherwise. 18 | 19 | You can use this template to get started: 20 | 21 | .. sourcecode:: python 22 | 23 | import re 24 | 25 | def test(string): 26 | # Your code goes here 27 | -------------------------------------------------------------------------------- /learnregex/capturing/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.match(r'(.*)\|\1$', string) 10 | 11 | -------------------------------------------------------------------------------- /learnregex/capturing/__init__.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from story.adventures import AdventureVerificationError, BaseAdventure 4 | from story.translation import gettext as _ 5 | 6 | from ..utils import get_random_string, load_solution_function 7 | 8 | 9 | class Adventure(BaseAdventure): 10 | 11 | title = _('Capturing') 12 | dictionary = string.ascii_lowercase + string.digits 13 | 14 | def test(self, file): 15 | function = load_solution_function(file) 16 | repeat = get_random_string(self.dictionary, 4, 6) 17 | correct_argument = '{0}|{0}'.format(repeat) 18 | if not function(correct_argument): 19 | raise AdventureVerificationError( 20 | _("Your function didn't return True when executed with a " 21 | "correct argument '{}'.".format(correct_argument)) 22 | ) 23 | wrong_argument = '{}|{}'.format( 24 | get_random_string(self.dictionary, 5, 5), 25 | get_random_string(self.dictionary, 5, 5) 26 | ) 27 | if function(wrong_argument): 28 | raise AdventureVerificationError( 29 | _("Your function returned True when executed with a wrong " 30 | "argument '{}'.".format(wrong_argument))) 31 | -------------------------------------------------------------------------------- /learnregex/character_classes/README.rst: -------------------------------------------------------------------------------- 1 | Now that we now what special characters are and how to escape them, let's try 2 | their actual special meaning. We are going to start with character classes. 3 | 4 | A character class matches one out of a set of characters that we define. You 5 | define a character class by writing all the characters of your set between 6 | square brackets. For example, this matches either an "a" or a "b": [ba] (the 7 | order doesn't matter). 8 | 9 | For this adventure, write a python function that receives a string and 10 | returns `True` if the first character is a digit, and `False` otherwise. 11 | 12 | You can use this template to get started: 13 | 14 | .. sourcecode:: python 15 | 16 | import re 17 | 18 | def test(string): 19 | # Your code goes here 20 | 21 | When you are done, you must run: 22 | 23 | .. sourcecode:: bash 24 | 25 | $ {script} verify program.py 26 | -------------------------------------------------------------------------------- /learnregex/character_classes/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.match(r'[0123456789]', string) 10 | 11 | -------------------------------------------------------------------------------- /learnregex/character_classes/__init__.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | from story.adventures import AdventureVerificationError, BaseAdventure 5 | from story.translation import gettext as _ 6 | 7 | from ..utils import get_random_string, load_solution_function 8 | 9 | 10 | class Adventure(BaseAdventure): 11 | 12 | title = _('Character classes') 13 | dictionary = string.ascii_lowercase 14 | 15 | def test(self, file): 16 | function = load_solution_function(file) 17 | correct_argument = '{}{}'.format( 18 | random.randint(0, 9), 19 | get_random_string(self.dictionary, 1, 5) 20 | ) 21 | if not function(correct_argument): 22 | raise AdventureVerificationError( 23 | _("Your function didn't return True when executed with a " 24 | "correct argument '{}'.".format(correct_argument)) 25 | ) 26 | wrong_argument = get_random_string(self.dictionary, 1, 5) 27 | if function(wrong_argument): 28 | raise AdventureVerificationError( 29 | _("Your function returned True when executed with a wrong " 30 | "argument '{}'.".format(wrong_argument))) 31 | -------------------------------------------------------------------------------- /learnregex/greediness/README.rst: -------------------------------------------------------------------------------- 1 | When using quantifiers sometimes we'll run into an issue where they match 2 | more than we want. Suppose we want to match the first part (including the 3 | dot) of a domain. We write the expression '.*\.' thinking "this will return 4 | all the characters before the first dot along with it" and test it on 5 | "pyschool.github.io" expecting to get "pyschool." back, but it returns 6 | "pyschool.github.". 7 | 8 | What happened? Quantifiers are greedy by default, meaning they will match 9 | all the characters they can. In this case we have two dots in our string, so 10 | the quantifier gets to match all the characters up to the last dot without 11 | problem. 12 | 13 | We can make our quantifiers "lazy" by adding a question mark at their end 14 | (even the ? quantifier, which becomes ?? in its lazy form), meaning they will 15 | match the minimum amount of characters they can. If we add a question mark to 16 | our quantifier it becomes lazy and will now match all the characters up to 17 | the first dot and stop there, since the whole expression matches. 18 | 19 | For this adventure, write a python function that receives a string of comma 20 | separated values and returns all the characters from the start up to the 21 | first comma, including it. 22 | 23 | You can use this template to get started: 24 | 25 | .. sourcecode:: python 26 | 27 | import re 28 | 29 | def test(string): 30 | # Your code goes here 31 | 32 | When you are done, you must run: 33 | 34 | .. sourcecode:: bash 35 | 36 | $ {script} verify program.py 37 | -------------------------------------------------------------------------------- /learnregex/greediness/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.match(r'.*?,', string).group(0) 10 | 11 | -------------------------------------------------------------------------------- /learnregex/greediness/__init__.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from story.adventures import AdventureVerificationError, BaseAdventure 4 | from story.translation import gettext as _ 5 | 6 | from ..utils import get_random_string, load_solution_function 7 | 8 | 9 | class Adventure(BaseAdventure): 10 | 11 | title = _('Greediness') 12 | dictionary = string.ascii_lowercase + string.digits 13 | 14 | def test(self, file): 15 | function = load_solution_function(file) 16 | prefix = get_random_string(self.dictionary, 1, 5) + ',' 17 | correct_argument = '{}{},{}'.format( 18 | prefix, 19 | get_random_string(self.dictionary, 1, 5), 20 | get_random_string(self.dictionary, 1, 5), 21 | ) 22 | result = function(correct_argument) 23 | if result != prefix: 24 | raise AdventureVerificationError( 25 | _("Your function didn't return the expected string '{}' when " 26 | "executed with '{}'. " 27 | "It returned '{}'.".format(prefix, correct_argument, result)) 28 | ) 29 | -------------------------------------------------------------------------------- /learnregex/groups/README.rst: -------------------------------------------------------------------------------- 1 | Groups have plenty of uses. They basically enclose a section of our 2 | expression so we can treat it as a token. This allows us to apply quantifiers 3 | to a whole expression instead of a single character or character class like 4 | we have been doing. It also allows us to use the alternation operator with a 5 | limited scope. 6 | 7 | Groups are defined with simple parenthesis. We can put everything inside them, 8 | even other groups. 9 | 10 | For this adventure, write a python function that receives a string and 11 | returns `True` if starts with one or more repetitions of the word 'hello' and 12 | ends with either 'python' or 'pyschool', and `False` otherwise. You don't 13 | need to check for spaces, the words just need to follow one immediately after 14 | another. 15 | 16 | You can use this template to get started: 17 | 18 | .. sourcecode:: python 19 | 20 | import re 21 | 22 | def test(string): 23 | # Your code goes here 24 | -------------------------------------------------------------------------------- /learnregex/groups/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.match(r'(hello)+(python|pyschool)', string) 10 | 11 | -------------------------------------------------------------------------------- /learnregex/groups/__init__.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from story.adventures import AdventureVerificationError, BaseAdventure 4 | from story.translation import gettext as _ 5 | 6 | from ..utils import load_solution_function 7 | 8 | 9 | class Adventure(BaseAdventure): 10 | 11 | title = _('Groups') 12 | 13 | def test(self, file): 14 | function = load_solution_function(file) 15 | correct_argument = 'hello' * random.randint(1, 5) + random.choice( 16 | ['python', 'pyschool'] 17 | ) 18 | if not function(correct_argument): 19 | raise AdventureVerificationError( 20 | _("Your function didn't return True when executed with a " 21 | "correct argument '{}'.".format(correct_argument)) 22 | ) 23 | wrong_argument = random.choice(['python', 'pyschool']) 24 | if function(wrong_argument): 25 | raise AdventureVerificationError( 26 | _("Your function returned True when executed with a wrong " 27 | "argument '{}'.".format(wrong_argument))) 28 | -------------------------------------------------------------------------------- /learnregex/introduction/README.rst: -------------------------------------------------------------------------------- 1 | Regular expressions (or regex for short) are a little programming language 2 | that comes embedded in many systems, applications and even other programming 3 | languages as Python. Unlike most programming languages, regular expressions 4 | have a particular and limited scope: matching strings. 5 | 6 | In Python, regular expressions are provided by the `re` module. Be sure to use 7 | it in every adventure and keep its documentation_ at hand so you know how to 8 | do so. 9 | 10 | .. _documentation: https://docs.python.org/3/library/re.html 11 | 12 | For this first adventure, write a python function that receives a string and 13 | returns True if it contains the substring 'Python', and False otherwise. 14 | 15 | You can use this template to get started: 16 | 17 | .. sourcecode:: python 18 | 19 | import re 20 | 21 | def test(string): 22 | # Your code goes here 23 | 24 | When you are done, you must run: 25 | 26 | .. sourcecode:: bash 27 | 28 | $ {script} verify program.py 29 | 30 | Your program will be tested and the lesson will be marked 'completed' if you 31 | are successful. 32 | -------------------------------------------------------------------------------- /learnregex/introduction/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.search('Python', string) 10 | -------------------------------------------------------------------------------- /learnregex/introduction/__init__.py: -------------------------------------------------------------------------------- 1 | from story.adventures import AdventureVerificationError, BaseAdventure 2 | from story.translation import gettext as _ 3 | 4 | from ..utils import load_solution_function 5 | 6 | 7 | class Adventure(BaseAdventure): 8 | 9 | title = _('Introduction') 10 | 11 | def test(self, file): 12 | function = load_solution_function(file) 13 | correct_argument = 'Python' 14 | if not function(correct_argument): 15 | raise AdventureVerificationError( 16 | _("Your function didn't return True when executed with a " 17 | "correct argument '{}'.".format(correct_argument)) 18 | ) 19 | wrong_argument = 'ython' 20 | if function(wrong_argument): 21 | raise AdventureVerificationError( 22 | _("Your function returned True when executed with a wrong " 23 | "argument '{}'.".format(wrong_argument))) 24 | -------------------------------------------------------------------------------- /learnregex/locale/en/LC_MESSAGES/pyschool.po: -------------------------------------------------------------------------------- 1 | # English translations for PROJECT. 2 | # Copyright (C) 2017 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2017. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2017-04-16 22:41-0300\n" 11 | "PO-Revision-Date: 2017-04-16 21:13-0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: en\n" 14 | "Language-Team: en \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.4.0\n" 20 | 21 | #: learnregex/story.py:12 22 | msgid "Learn regular expressions with Python" 23 | msgstr "" 24 | 25 | #: learnregex/utils.py:23 26 | msgid "" 27 | "I can't import a module from {}. Are you sure the file exists and it's a " 28 | "valid python file?" 29 | msgstr "" 30 | 31 | #: learnregex/utils.py:27 32 | msgid "" 33 | "I can't find a function definition in {}. Read the adventure again and " 34 | "use the template proposed there." 35 | msgstr "" 36 | 37 | #: learnregex/utils.py:34 38 | msgid "" 39 | "I found a function in your file but it doesn't accept the required number" 40 | " of parameters ({}). If you define several functions in your file, make " 41 | "sure the solution function is the first one." 42 | msgstr "" 43 | 44 | #: learnregex/alternation/__init__.py:11 45 | msgid "Alternation" 46 | msgstr "" 47 | 48 | #: learnregex/alternation/__init__.py:20 learnregex/anchors/__init__.py:20 49 | #: learnregex/capturing/__init__.py:21 50 | #: learnregex/character_classes/__init__.py:24 learnregex/groups/__init__.py:21 51 | #: learnregex/introduction/__init__.py:17 52 | #: learnregex/negated_character_classes/__init__.py:21 53 | #: learnregex/quantifiers/__init__.py:23 54 | #: learnregex/special_characters/__init__.py:23 55 | #: learnregex/the_dot/__init__.py:22 56 | msgid "" 57 | "Your function didn't return True when executed with a correct argument " 58 | "'{}'." 59 | msgstr "" 60 | 61 | #: learnregex/alternation/__init__.py:27 learnregex/anchors/__init__.py:26 62 | #: learnregex/capturing/__init__.py:30 63 | #: learnregex/character_classes/__init__.py:30 learnregex/groups/__init__.py:27 64 | #: learnregex/introduction/__init__.py:23 65 | #: learnregex/negated_character_classes/__init__.py:30 66 | #: learnregex/quantifiers/__init__.py:29 67 | #: learnregex/special_characters/__init__.py:29 68 | #: learnregex/the_dot/__init__.py:28 69 | msgid "Your function returned True when executed with a wrong argument '{}'." 70 | msgstr "" 71 | 72 | #: learnregex/anchors/__init__.py:11 73 | msgid "Anchors" 74 | msgstr "" 75 | 76 | #: learnregex/capturing/__init__.py:11 77 | msgid "Capturing" 78 | msgstr "" 79 | 80 | #: learnregex/character_classes/__init__.py:12 81 | msgid "Character classes" 82 | msgstr "" 83 | 84 | #: learnregex/greediness/__init__.py:11 85 | msgid "Greediness" 86 | msgstr "" 87 | 88 | #: learnregex/greediness/__init__.py:27 89 | msgid "" 90 | "Your function didn't return the expected string '{}' when executed with " 91 | "'{}'. It returned '{}'." 92 | msgstr "" 93 | 94 | #: learnregex/groups/__init__.py:11 95 | msgid "Groups" 96 | msgstr "" 97 | 98 | #: learnregex/introduction/__init__.py:9 99 | msgid "Introduction" 100 | msgstr "" 101 | 102 | #: learnregex/negated_character_classes/__init__.py:12 103 | msgid "Negated character classes" 104 | msgstr "" 105 | 106 | #: learnregex/quantifiers/__init__.py:11 107 | msgid "Quantifiers" 108 | msgstr "" 109 | 110 | #: learnregex/special_characters/__init__.py:11 111 | msgid "Special characters" 112 | msgstr "" 113 | 114 | #: learnregex/the_dot/__init__.py:11 115 | msgid "The dot" 116 | msgstr "" 117 | 118 | -------------------------------------------------------------------------------- /learnregex/locale/es/LC_MESSAGES/pyschool.po: -------------------------------------------------------------------------------- 1 | # Spanish translations for PROJECT. 2 | # Copyright (C) 2017 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2017. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2017-04-16 22:41-0300\n" 11 | "PO-Revision-Date: 2017-04-16 21:13-0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: es\n" 14 | "Language-Team: es \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.4.0\n" 20 | 21 | #: learnregex/story.py:12 22 | msgid "Learn regular expressions with Python" 23 | msgstr "" 24 | 25 | #: learnregex/utils.py:23 26 | msgid "" 27 | "I can't import a module from {}. Are you sure the file exists and it's a " 28 | "valid python file?" 29 | msgstr "" 30 | 31 | #: learnregex/utils.py:27 32 | msgid "" 33 | "I can't find a function definition in {}. Read the adventure again and " 34 | "use the template proposed there." 35 | msgstr "" 36 | 37 | #: learnregex/utils.py:34 38 | msgid "" 39 | "I found a function in your file but it doesn't accept the required number" 40 | " of parameters ({}). If you define several functions in your file, make " 41 | "sure the solution function is the first one." 42 | msgstr "" 43 | 44 | #: learnregex/alternation/__init__.py:11 45 | msgid "Alternation" 46 | msgstr "" 47 | 48 | #: learnregex/alternation/__init__.py:20 learnregex/anchors/__init__.py:20 49 | #: learnregex/capturing/__init__.py:21 50 | #: learnregex/character_classes/__init__.py:24 learnregex/groups/__init__.py:21 51 | #: learnregex/introduction/__init__.py:17 52 | #: learnregex/negated_character_classes/__init__.py:21 53 | #: learnregex/quantifiers/__init__.py:23 54 | #: learnregex/special_characters/__init__.py:23 55 | #: learnregex/the_dot/__init__.py:22 56 | msgid "" 57 | "Your function didn't return True when executed with a correct argument " 58 | "'{}'." 59 | msgstr "" 60 | 61 | #: learnregex/alternation/__init__.py:27 learnregex/anchors/__init__.py:26 62 | #: learnregex/capturing/__init__.py:30 63 | #: learnregex/character_classes/__init__.py:30 learnregex/groups/__init__.py:27 64 | #: learnregex/introduction/__init__.py:23 65 | #: learnregex/negated_character_classes/__init__.py:30 66 | #: learnregex/quantifiers/__init__.py:29 67 | #: learnregex/special_characters/__init__.py:29 68 | #: learnregex/the_dot/__init__.py:28 69 | msgid "Your function returned True when executed with a wrong argument '{}'." 70 | msgstr "" 71 | 72 | #: learnregex/anchors/__init__.py:11 73 | msgid "Anchors" 74 | msgstr "" 75 | 76 | #: learnregex/capturing/__init__.py:11 77 | msgid "Capturing" 78 | msgstr "" 79 | 80 | #: learnregex/character_classes/__init__.py:12 81 | msgid "Character classes" 82 | msgstr "" 83 | 84 | #: learnregex/greediness/__init__.py:11 85 | msgid "Greediness" 86 | msgstr "" 87 | 88 | #: learnregex/greediness/__init__.py:27 89 | msgid "" 90 | "Your function didn't return the expected string '{}' when executed with " 91 | "'{}'. It returned '{}'." 92 | msgstr "" 93 | 94 | #: learnregex/groups/__init__.py:11 95 | msgid "Groups" 96 | msgstr "" 97 | 98 | #: learnregex/introduction/__init__.py:9 99 | msgid "Introduction" 100 | msgstr "" 101 | 102 | #: learnregex/negated_character_classes/__init__.py:12 103 | msgid "Negated character classes" 104 | msgstr "" 105 | 106 | #: learnregex/quantifiers/__init__.py:11 107 | msgid "Quantifiers" 108 | msgstr "" 109 | 110 | #: learnregex/special_characters/__init__.py:11 111 | msgid "Special characters" 112 | msgstr "" 113 | 114 | #: learnregex/the_dot/__init__.py:11 115 | msgid "The dot" 116 | msgstr "El punto" 117 | 118 | -------------------------------------------------------------------------------- /learnregex/negated_character_classes/README.rst: -------------------------------------------------------------------------------- 1 | When the first character inside a character class is a caret (remember this 2 | is a special character), it transforms our character class into a negated 3 | character class, meaning it will match any character that's not in our set. 4 | 5 | For this adventure, write a python function that receives a string and 6 | returns `True` if the first character is not a number, and `False` otherwise. 7 | 8 | You can use this template to get started: 9 | 10 | .. sourcecode:: python 11 | 12 | import re 13 | 14 | def test(string): 15 | # Your code goes here 16 | 17 | When you are done, you must run: 18 | 19 | .. sourcecode:: bash 20 | 21 | $ {script} verify program.py 22 | -------------------------------------------------------------------------------- /learnregex/negated_character_classes/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.match(r'[^0123456789]', string) 10 | 11 | -------------------------------------------------------------------------------- /learnregex/negated_character_classes/__init__.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | from story.adventures import AdventureVerificationError, BaseAdventure 5 | from story.translation import gettext as _ 6 | 7 | from ..utils import get_random_string, load_solution_function 8 | 9 | 10 | class Adventure(BaseAdventure): 11 | 12 | title = _('Negated character classes') 13 | dictionary = string.ascii_lowercase 14 | 15 | def test(self, file): 16 | function = load_solution_function(file) 17 | correct_argument = get_random_string(self.dictionary, 1, 5) 18 | if not function(correct_argument): 19 | raise AdventureVerificationError( 20 | _("Your function didn't return True when executed with a " 21 | "correct argument '{}'.".format(correct_argument)) 22 | ) 23 | wrong_argument = '{}{}'.format( 24 | random.randint(0, 9), 25 | get_random_string(self.dictionary, 1, 5) 26 | ) 27 | if function(wrong_argument): 28 | raise AdventureVerificationError( 29 | _("Your function returned True when executed with a wrong " 30 | "argument '{}'.".format(wrong_argument))) 31 | -------------------------------------------------------------------------------- /learnregex/quantifiers/README.rst: -------------------------------------------------------------------------------- 1 | This is one of the most interesting features of regular expressions. It 2 | allows us to match any number of repetitions of a character (or character 3 | class, or sequence of characters). We have three predefined quantifiers that 4 | cover the most common use cases plus a fourth generic one which is a bit 5 | longer, but covers all possible cases. 6 | 7 | ?: match the preceding token zero or one time. 8 | *: match the preceding token zero or more times. 9 | +: match the preceding token one or more times. 10 | {{min,max}}: match the preceding token n times such that min <= n <= max. You 11 | can omit the max (but leaving the comma) to make it infinite. If you omit 12 | both the comma and the max, it matches the preceding token exactly "min" 13 | times. 14 | 15 | For this adventure, write a python function that receives a string and 16 | returns `True` if it starts with a digit, followed by zero or more spaces, 17 | followed by another digit; and `False` otherwise. 18 | 19 | You can use this template to get started: 20 | 21 | .. sourcecode:: python 22 | 23 | import re 24 | 25 | def test(string): 26 | # Your code goes here 27 | 28 | When you are done, you must run: 29 | 30 | .. sourcecode:: bash 31 | 32 | $ {script} verify program.py 33 | -------------------------------------------------------------------------------- /learnregex/quantifiers/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.match(r'[0123456789] *[0123456789]', string) 10 | 11 | -------------------------------------------------------------------------------- /learnregex/quantifiers/__init__.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from story.adventures import AdventureVerificationError, BaseAdventure 4 | from story.translation import gettext as _ 5 | 6 | from ..utils import load_solution_function 7 | 8 | 9 | class Adventure(BaseAdventure): 10 | 11 | title = _('Quantifiers') 12 | 13 | def test(self, file): 14 | function = load_solution_function(file) 15 | correct_argument = '{}{}{}'.format( 16 | random.randint(0, 9), 17 | ' ' * random.randint(0, 5), 18 | random.randint(0, 9) 19 | ) 20 | if not function(correct_argument): 21 | raise AdventureVerificationError( 22 | _("Your function didn't return True when executed with a " 23 | "correct argument '{}'.".format(correct_argument)) 24 | ) 25 | wrong_argument = str(random.randint(0, 9)) 26 | if function(wrong_argument): 27 | raise AdventureVerificationError( 28 | _("Your function returned True when executed with a wrong " 29 | "argument '{}'.".format(wrong_argument))) 30 | -------------------------------------------------------------------------------- /learnregex/special_characters/README.rst: -------------------------------------------------------------------------------- 1 | Searching for literal strings is really simple and we wouldn't really need 2 | regular expressions to do it. Where regular expressions really shine is in 3 | matching patterns, and to do so it must define a set of characters with 4 | special meaning. 5 | 6 | The following characters are considered special and you must "escape" them if 7 | you want to use them literally. 8 | 9 | .. sourcecode:: bash 10 | 11 | . ? * + ^ $ ( ) [ ] {{ }} | \ 12 | 13 | The closing square bracket ] and closing curly brace }} can be used 14 | literally without escaping them and their meaning will be determined by the 15 | presence of their opening counterparts, but it's clearer to escape them every 16 | time you want to use them literally. 17 | 18 | To escape a special character you put a backslash (here the backslash is 19 | using its special meaning!) before them. 20 | 21 | For this adventure, write a python function that receives a string and 22 | returns `True` if it contains a backslash, and `False` otherwise. 23 | 24 | You can use this template to get started: 25 | 26 | .. sourcecode:: python 27 | 28 | import re 29 | 30 | def test(string): 31 | # Your code goes here 32 | 33 | HINT 34 | ____ 35 | 36 | The backslash is also a special character in Python's string literals. To 37 | avoid issues, define your regular expressions as "raw strings". 38 | 39 | Normal string: 'hello' 40 | Raw string: r'hello' 41 | 42 | When you are done, you must run: 43 | 44 | .. sourcecode:: bash 45 | 46 | $ {script} verify program.py 47 | -------------------------------------------------------------------------------- /learnregex/special_characters/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.search(r'\\', string) 10 | 11 | -------------------------------------------------------------------------------- /learnregex/special_characters/__init__.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from story.adventures import AdventureVerificationError, BaseAdventure 4 | from story.translation import gettext as _ 5 | 6 | from ..utils import get_random_string, load_solution_function 7 | 8 | 9 | class Adventure(BaseAdventure): 10 | 11 | title = _('Special characters') 12 | dictionary = string.ascii_lowercase + string.digits 13 | 14 | def test(self, file): 15 | function = load_solution_function(file) 16 | correct_argument = '{}\{}'.format( 17 | get_random_string(self.dictionary, 0, 5), 18 | get_random_string(self.dictionary, 0, 5) 19 | ) 20 | if not function(correct_argument): 21 | raise AdventureVerificationError( 22 | _("Your function didn't return True when executed with a " 23 | "correct argument '{}'.".format(correct_argument)) 24 | ) 25 | wrong_argument = get_random_string(self.dictionary, 1, 5) 26 | if function(wrong_argument): 27 | raise AdventureVerificationError( 28 | _("Your function returned True when executed with a wrong " 29 | "argument '{}'.".format(wrong_argument))) 30 | -------------------------------------------------------------------------------- /learnregex/the_dot/README.rst: -------------------------------------------------------------------------------- 1 | The dot matches any character. Consider it like a character class with all 2 | existing characters in it. 3 | 4 | For this adventure, write a python function that receives a string and 5 | returns `True` if it starts with 'python', followed by any character, 6 | followed by 'python' again; and `False` otherwise. 7 | 8 | You can use this template to get started: 9 | 10 | .. sourcecode:: python 11 | 12 | import re 13 | 14 | def test(string): 15 | # Your code goes here 16 | 17 | When you are done, you must run: 18 | 19 | .. sourcecode:: bash 20 | 21 | $ {script} verify program.py 22 | -------------------------------------------------------------------------------- /learnregex/the_dot/SOLUTION.rst: -------------------------------------------------------------------------------- 1 | `program.py` content: 2 | 3 | .. sourcecode:: python 4 | 5 | import re 6 | 7 | 8 | def test(string): 9 | return re.match(r'python.python', string) 10 | 11 | -------------------------------------------------------------------------------- /learnregex/the_dot/__init__.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from story.adventures import AdventureVerificationError, BaseAdventure 4 | from story.translation import gettext as _ 5 | 6 | from ..utils import get_random_string, load_solution_function 7 | 8 | 9 | class Adventure(BaseAdventure): 10 | 11 | title = _('The dot') 12 | dictionary = string.ascii_lowercase + string.digits + string.punctuation 13 | 14 | def test(self, file): 15 | function = load_solution_function(file) 16 | correct_argument = 'python{}python'.format( 17 | get_random_string(self.dictionary, 1, 1) 18 | ) 19 | if not function(correct_argument): 20 | raise AdventureVerificationError( 21 | _("Your function didn't return True when executed with a " 22 | "correct argument '{}'.".format(correct_argument)) 23 | ) 24 | wrong_argument = 'pythonpython' 25 | if function(wrong_argument): 26 | raise AdventureVerificationError( 27 | _("Your function returned True when executed with a wrong " 28 | "argument '{}'.".format(wrong_argument))) 29 | -------------------------------------------------------------------------------- /learnregex/utils.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import random 4 | import sys 5 | import os 6 | 7 | from story.adventures import AdventureVerificationError 8 | from story.translation import gettext as _ 9 | 10 | 11 | def load_solution_function(file, parameters=1): 12 | try: 13 | path, file = os.path.split(os.path.abspath(file)) 14 | sys.path.append(path) 15 | if file.endswith('.py'): 16 | module_name = file[:-3] 17 | elif file.endswith('.pyc'): 18 | module_name = file[:-4] 19 | else: 20 | module_name = file 21 | module = importlib.import_module(module_name) 22 | function = inspect.getmembers(module, inspect.isfunction)[0][1] 23 | assert len(inspect.signature(function).parameters) == parameters 24 | return function 25 | except ImportError: 26 | raise AdventureVerificationError( 27 | _('I can\'t import a module from {}. Are you sure the file exists ' 28 | 'and it\'s a valid python file?').format(file)) 29 | except IndexError: 30 | raise AdventureVerificationError( 31 | _('I can\'t find a function definition in {}. Read the adventure ' 32 | 'again and use the template proposed there.').format(file)) 33 | except AssertionError: 34 | raise AdventureVerificationError( 35 | _('I found a function in your file but it doesn\'t accept the ' 36 | 'required number of parameters ({}). If you define several ' 37 | 'functions in your file, make sure the solution function is the ' 38 | 'first one.'.format(parameters))) 39 | 40 | 41 | def get_random_string(dictionary, min_length, max_length): 42 | result = '' 43 | length = random.randint(min_length, max_length) 44 | while len(result) < length: 45 | result += random.choice(dictionary) 46 | return result 47 | -------------------------------------------------------------------------------- /requirements_dev.pip: -------------------------------------------------------------------------------- 1 | pip==9.0.1 2 | bumpversion==0.5.3 3 | wheel==0.29.0 4 | watchdog==0.8.3 5 | flake8==3.3.0 6 | tox==2.7.0 7 | coverage==4.3.4 8 | cryptography==1.8.1 9 | PyYAML==3.12 10 | pytest==3.0.7 11 | Babel==2.4.0 12 | story==1.2.2 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.4.11 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:learnregex/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | with open('README.rst') as readme_file: 7 | readme = readme_file.read() 8 | 9 | with open('HISTORY.rst') as history_file: 10 | history = history_file.read() 11 | 12 | setup( 13 | name='learnregex', 14 | version='0.4.11', 15 | description='A pyschool story for learning regular expressions.', 16 | long_description=readme + '\n\n' + history, 17 | author='Sophilabs', 18 | author_email='hi@sophilabs.co', 19 | url='https://github.com/sophilabs/learnregex', 20 | packages=['learnregex'], 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'learnregex=learnregex:Story.begin' 24 | ] 25 | }, 26 | include_package_data=True, 27 | install_requires=[ 28 | 'story>=1.2.2' 29 | ], 30 | python_requires='!=2, !=3.0.*, !=3.1.*, !=3.2.*, <4', 31 | license='MIT license', 32 | zip_safe=False, 33 | keywords='learnregex', 34 | classifiers=[ 35 | 'Development Status :: 2 - Pre-Alpha', 36 | 'Intended Audience :: Developers', 37 | 'License :: OSI Approved :: MIT License', 38 | 'Natural Language :: English', 39 | 'Programming Language :: Python :: 3', 40 | 'Programming Language :: Python :: 3.3', 41 | 'Programming Language :: Python :: 3.4', 42 | 'Programming Language :: Python :: 3.5', 43 | ], 44 | test_suite='tests', 45 | tests_require=[] 46 | ) 47 | -------------------------------------------------------------------------------- /tests/test_story.py: -------------------------------------------------------------------------------- 1 | from learnregex import Story 2 | 3 | 4 | class TestStory(object): 5 | 6 | def test_name(self): 7 | assert Story().name == 'learnregex' 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py33, py34, py35, flake8 3 | 4 | [testenv:flake8] 5 | basepython=python 6 | deps=flake8 7 | commands=flake8 learnregex 8 | 9 | [testenv] 10 | setenv = 11 | PYTHONPATH = {toxinidir}:{toxinidir}/learnregex 12 | deps = 13 | -r{toxinidir}/requirements_dev.pip 14 | commands = 15 | pip install -U pip 16 | pybabel compile -D pyschool -d learnregex/locale -f --statistics 17 | py.test --basetemp={envtmpdir} 18 | 19 | ; If you want to make tox run the tests with the same versions, create a 20 | ; requirements.txt with the pinned versions and uncomment the following lines: 21 | ; deps = 22 | ; -r{toxinidir}/requirements.txt 23 | -------------------------------------------------------------------------------- /travis_pypi_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Update encrypted deploy password in Travis config file 4 | """ 5 | 6 | 7 | from __future__ import print_function 8 | import base64 9 | import json 10 | import os 11 | from getpass import getpass 12 | import yaml 13 | from cryptography.hazmat.primitives.serialization import load_pem_public_key 14 | from cryptography.hazmat.backends import default_backend 15 | from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 16 | 17 | 18 | try: 19 | from urllib import urlopen 20 | except: 21 | from urllib.request import urlopen 22 | 23 | 24 | GITHUB_REPO = 'pyschool/story' 25 | TRAVIS_CONFIG_FILE = os.path.join( 26 | os.path.dirname(os.path.abspath(__file__)), '.travis.yml') 27 | 28 | 29 | def load_key(pubkey): 30 | """Load public RSA key, with work-around for keys using 31 | incorrect header/footer format. 32 | 33 | Read more about RSA encryption with cryptography: 34 | https://cryptography.io/latest/hazmat/primitives/asymmetric/rsa/ 35 | """ 36 | try: 37 | return load_pem_public_key(pubkey.encode(), default_backend()) 38 | except ValueError: 39 | # workaround for https://github.com/travis-ci/travis-api/issues/196 40 | pubkey = pubkey.replace('BEGIN RSA', 'BEGIN').replace('END RSA', 'END') 41 | return load_pem_public_key(pubkey.encode(), default_backend()) 42 | 43 | 44 | def encrypt(pubkey, password): 45 | """Encrypt password using given RSA public key and encode it with base64. 46 | 47 | The encrypted password can only be decrypted by someone with the 48 | private key (in this case, only Travis). 49 | """ 50 | key = load_key(pubkey) 51 | encrypted_password = key.encrypt(password, PKCS1v15()) 52 | return base64.b64encode(encrypted_password) 53 | 54 | 55 | def fetch_public_key(repo): 56 | """Download RSA public key Travis will use for this repo. 57 | 58 | Travis API docs: http://docs.travis-ci.com/api/#repository-keys 59 | """ 60 | keyurl = 'https://api.travis-ci.org/repos/{0}/key'.format(repo) 61 | data = json.loads(urlopen(keyurl).read().decode()) 62 | if 'key' not in data: 63 | errmsg = "Could not find public key for repo: {}.\n".format(repo) 64 | errmsg += "Have you already added your GitHub repo to Travis?" 65 | raise ValueError(errmsg) 66 | return data['key'] 67 | 68 | 69 | def prepend_line(filepath, line): 70 | """Rewrite a file adding a line to its beginning. 71 | """ 72 | with open(filepath) as f: 73 | lines = f.readlines() 74 | 75 | lines.insert(0, line) 76 | 77 | with open(filepath, 'w') as f: 78 | f.writelines(lines) 79 | 80 | 81 | def load_yaml_config(filepath): 82 | with open(filepath) as f: 83 | return yaml.load(f) 84 | 85 | 86 | def save_yaml_config(filepath, config): 87 | with open(filepath, 'w') as f: 88 | yaml.dump(config, f, default_flow_style=False) 89 | 90 | 91 | def update_travis_deploy_password(encrypted_password): 92 | """Update the deploy section of the .travis.yml file 93 | to use the given encrypted password. 94 | """ 95 | config = load_yaml_config(TRAVIS_CONFIG_FILE) 96 | 97 | config['deploy']['password'] = dict(secure=encrypted_password) 98 | 99 | save_yaml_config(TRAVIS_CONFIG_FILE, config) 100 | 101 | line = ('# This file was autogenerated and will overwrite' 102 | ' each time you run travis_pypi_setup.py\n') 103 | prepend_line(TRAVIS_CONFIG_FILE, line) 104 | 105 | 106 | def main(args): 107 | public_key = fetch_public_key(args.repo) 108 | password = args.password or getpass('PyPI password: ') 109 | update_travis_deploy_password(encrypt(public_key, password.encode())) 110 | print("Wrote encrypted password to .travis.yml -- you're ready to deploy") 111 | 112 | 113 | if '__main__' == __name__: 114 | import argparse 115 | parser = argparse.ArgumentParser(description=__doc__) 116 | parser.add_argument('--repo', default=GITHUB_REPO, 117 | help='GitHub repo (default: %s)' % GITHUB_REPO) 118 | parser.add_argument('--password', 119 | help='PyPI password (will prompt if not provided)') 120 | 121 | args = parser.parse_args() 122 | main(args) 123 | --------------------------------------------------------------------------------