Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue-172 query runner version and fix ruby bug Issue-162 #173

Merged
merged 6 commits into from
May 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ["2.4", "2.5", "2.6"]
ruby-version: ["2.4", "2.5", "2.6", "2.7", "3.0"]
steps:
- uses: actions/checkout@v2
- name: Setup python
Expand Down
31 changes: 25 additions & 6 deletions byexample/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,21 +150,29 @@ def tohuman(s):
return s


def constant(argumentless_method):
def constant(method):
''' Cache the result of calling the method in the first call
and save the result in <self>.

The method must always return the same results and the result
itself must be an immutable object so it is safe to cache the
result and share it among different threads.

Further calls to the method will yield the same result even
if the arguments or <self> differ.

If you want to cache the different values returned under different
arguments, see functools.lru_cache. However, byexample may not be
able to preserve correctness and thread-safety for it. (consider
that as a "TODO").
'''
placeholder = '_saved_constant_result_of_%s' % argumentless_method.__name__
placeholder = '_saved_constant_result_of_%s' % method.__name__

def wrapped(self):
def wrapped(self, *args, **kargs):
try:
return getattr(self, placeholder)
except AttributeError:
val = argumentless_method(self)
val = method(self, *args, **kargs)
setattr(self, placeholder, val)
return val

Expand Down Expand Up @@ -249,7 +257,7 @@ def abspath(*args):
class ShebangTemplate(string.Template):
delimiter = '%'

def quote_and_substitute(self, tokens):
def quote_and_substitute(self, tokens, joined=True):
'''
Quote each token to be suitable for shell expansion and then
perform a substitution in the template.
Expand Down Expand Up @@ -282,6 +290,16 @@ def quote_and_substitute(self, tokens):
>>> shebang = '/bin/sh -c \'%e %p %a >/dev/null\''
>>> print(ShebangTemplate(shebang).quote_and_substitute(tokens))
/bin/sh -c '/usr/bin/env '"'"'py'"'"'"'"'"'"'"'"'thon'"'"' -i -c '"'"'blue = '"'"'"'"'"'"'"'"'1'"'"'"'"'"'"'"'"''"'"' >/dev/null'

As seen, by default ShebangTemplate returns a string, a "joined"
version of all the elements expanded.

If you need it, you can skip that (suitable for calling
subprocess.call or similar without using a shell):
>>> shebang = '%e %p %a'
>>> l = ShebangTemplate(shebang).quote_and_substitute(tokens, joined=False)
>>> print(" --- ".join(l))
/usr/bin/env --- py'thon --- -i --- -c --- blue = '1'
'''

self._tokens = {}
Expand All @@ -307,7 +325,8 @@ def quote_and_substitute(self, tokens):

cmd.append(x)

return ' '.join(cmd)
cmd = ' '.join(cmd)
return cmd if joined else shlex.split(cmd)


class Countdown:
Expand Down
79 changes: 71 additions & 8 deletions byexample/modules/ruby.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@
"""

from __future__ import unicode_literals
import pexpect, sys, time
import pexpect, sys, time, subprocess
import byexample.regex as re
from byexample.common import constant
from byexample.log import clog
from byexample.parser import ExampleParser
from byexample.finder import ExampleFinder
from byexample.runner import ExampleRunner, PexpectMixin, ShebangTemplate
Expand Down Expand Up @@ -123,6 +124,12 @@ def extend_option_parser(self, parser):
help='print the expression\'s value (true); ' +\
'suppress it (false); or print it only ' +\
'if the example has a => (auto, the default)')
parser.add_flag(
"ruby-start-large-output-in-new-line",
default=False,
help="add a newline after the => if the output that follows " +\
"does not fit in a single line. (irb >= 1.2.2)"
)
return parser


Expand Down Expand Up @@ -168,23 +175,74 @@ def _detect_expression_print_expected(self, example):
def interact(self, example, options):
PexpectMixin.interact(self)

def get_default_cmd(self, *args, **kargs):
def get_default_cmd(self, version, *args, **kargs):
if version and version >= (1, 2, 0):
args = ['-f', '--nomultiline', '--nocolorize', '--noreadline']
else:
args = ['--noreadline']

return "%e %p %a", {'e': '/usr/bin/env', 'p': 'irb', 'a': args}

def get_default_version_cmd(self, *args, **kargs):
return "%e %p %a", {
'e': '/usr/bin/env',
'p': 'irb',
'a': ['--noreadline']
'a': ['--version']
}

def build_cmd(self, options, default_shebang, default_tokens, joined=True):
shebang, tokens = default_shebang, default_tokens
shebang = options['shebangs'].get(self.language, shebang)

return ShebangTemplate(shebang).quote_and_substitute(tokens, joined)

@constant
def version_regex(self):
return re.compile(
r'''
([^\d]|^)
(?P<major>\d+)
\.
(?P<minor>\d+)
(\. (?P<patch>\d+))?
([^\d]|$)
''', re.VERBOSE
)

@constant
def get_version(self, options):
cmd = self.build_cmd(
options, *self.get_default_version_cmd(), joined=False
)
try:
out = subprocess.check_output(cmd).decode(self.encoding)
m = self.version_regex().search(out)

version = (
int(m.group(k) or 0) for k in ("major", "minor", "patch")
)
version = tuple(version)

except Exception as err:
clog().info(
"Could not detect interpreter version: %s\nExecuted command: %s",
str(err), cmd
)
return None

return version

def initialize(self, options):
ruby_pretty_print = options['ruby_pretty_print']

# always/yes; never/no; autoetect normalization
# always/yes; never/no; autodetect normalization
self.expr_print_mode = options['ruby_expr_print']

shebang, tokens = self.get_default_cmd()
shebang = options['shebangs'].get(self.language, shebang)
newline_before_multiline_output = options[
'ruby_start_large_output_in_new_line']

cmd = ShebangTemplate(shebang).quote_and_substitute(tokens)
version = self.get_version(options)
cmd = self.build_cmd(options, *self.get_default_cmd(version))

dfl_timeout = options['x']['dfl_timeout']

Expand All @@ -194,8 +252,13 @@ def initialize(self, options):
# In RVM contexts the IRB's prompt mode is changed even
# if we force the mode from the command line (whe we spawn IRB)
# Make sure that the first thing executed restores the default prompt.
#
# Also, set if IRB should print or not a newline between the => marker
# and the output if it is larger than a single line
nl = "true" if newline_before_multiline_output else "false"
self._exec_and_wait(
'IRB.CurrentContext.prompt_mode = :DEFAULT\n',
'IRB.CurrentContext.prompt_mode = :DEFAULT\n' +
'IRB.CurrentContext.newline_before_multiline_output = %s\n' % nl,
options,
timeout=dfl_timeout
)
Expand Down
10 changes: 10 additions & 0 deletions byexample/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ def cancel(self, example, options):
'''
return False

def get_version(self, options):
'''
Return the version of the underlying interpreter or runner in form
of a tuple. Return None if no version was determined.

This method may be called several times: it may be beneficial to
cache the results.
'''
return None


class PexpectMixin(object):
def __init__(self, PS1_re, any_PS_re):
Expand Down
9 changes: 8 additions & 1 deletion docs/languages/ruby.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,16 @@ the option ``-ruby-pretty-print``
```

> **Changed** in ``byexample 8.0.0``: make sure that a ``Hash``
> is printted in a deterministic way with its keys sorted.
> is printed in a deterministic way with its keys sorted.
> Before ``byexample 8.0.0`` the order was undefined.

> **Changed** in `byexample 10.0.4`: IRB `> 1.2.2` adds a newline
> between the `=>` marker and the output if it spans more than one line.
> To maintain backward compatibility `byexample` will suppress that
> newline by default.
> If you don't want that you can pass `+ruby-start-large-output-in-new-line`
> flag in the [command line](/{{ site.uprefix }}/basic/options) with `-o`.

### The object returned

Because everything in Ruby is an expression, everything returns a result.
Expand Down