Compare commits

...

6 commits

Author SHA1 Message Date
Aanand Prasad
b3f23e2d17 Bump 1.2.0
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-09 15:29:36 +01:00
Ben Firshman
a467a8a094 Merge pull request #1261 from aanand/fix-vars-in-volume-paths
Fix vars in volume paths
(cherry picked from commit 4926f8aef6)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>

Conflicts:
	tests/unit/service_test.py
2015-04-09 15:26:10 +01:00
Aanand Prasad
78227c3c06 Merge pull request #1202 from aanand/jenkins-script
WIP: Jenkins script
(cherry picked from commit 853ce255ea)
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-01 11:00:51 -04:00
Daniel Nephin
e4e802d1f8 Merge pull request #1213 from moysesb/relative_build
Make value of 'build:' relative to the yml file.
(cherry picked from commit 0f70b8638f)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-01 11:00:48 -04:00
Aanand Prasad
b24a60ba9f Merge pull request #1226 from aanand/merge-multi-value-options
Merge multi-value options when extending
(cherry picked from commit e708f4f59d)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-01 11:00:45 -04:00
Aanand Prasad
461b600068 Merge pull request #1225 from aanand/fix-1222
When extending, `build` replaces `image` and vice versa
(cherry picked from commit 6dbe321a45)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-01 11:00:40 -04:00
20 changed files with 323 additions and 49 deletions

View file

@ -1,6 +1,62 @@
Change log Change log
========== ==========
1.2.0rc4 (2015-04-09)
---------------------
This is a release candidate for Compose 1.2.0.
On top of the changes listed below for RC1-RC3, the following bugs have been fixed:
- Environment variables and the user's home directory (~) were not expanding properly in `volumes` host paths.
1.2.0rc3 (2015-03-23)
---------------------
This is a release candidate for Compose 1.2.0.
On top of the changes listed below for RC1 and RC2, the following bugs have been fixed:
- When copying a service's configuration with `extends`, `image` and `build` could come into conflict, resulting in an error, as it makes no sense to have both defined. Each now overwrites the other: if a service with `image` defined is extended and `build` is added, the `image` entry will be removed.
- When copying a service's configuration with `extends`, if both services defined a multi-value option such as `ports` or `dns`, the original value would be completely discarded. They are now concatenated instead.
- When a relative path is supplied to `build`, it is treated as relative to the *directory of the configuration file*, not the directory that `docker-compose` is being run in. In the majority of cases, those are the same, but if you use the `-f|--file` argument to specify a configuration file in another directory, **this is a breaking change**.
1.2.0rc2 (2015-03-24)
---------------------
This is a release candidate for Compose 1.2.0.
On top of the changes listed below for RC1, a bug has been fixed where containers were being created with blank entries for "Dns" and "DnsSearch", causing DNS lookups from within a container to fail.
1.2.0rc1 (2015-03-23)
---------------------
This is a release candidate for Compose 1.2.0.
- `docker-compose.yml` now supports an `extends` option, which enables a service to inherit configuration from another service in another configuration file. This is really good for sharing common configuration between apps, or for configuring the same app for different environments. Here's the [documentation](https://github.com/docker/compose/blob/master/docs/yml.md#extends).
- When using Compose with a Swarm cluster, containers that depend on one another will be co-scheduled on the same node. This means that most Compose apps will now work out of the box, as long as they don't use `build`.
- Repeated invocations of `docker-compose up` when using Compose with a Swarm cluster now work reliably.
- Filenames in `env_file` and volume host paths in `volumes` are now treated as relative to the *directory of the configuration file*, not the directory that `docker-compose` is being run in. In the majority of cases, those are the same, but if you use the `-f|--file` argument to specify a configuration file in another directory, **this is a breaking change**.
- A service can now share another service's network namespace with `net: container:<service>`.
- `volumes_from` and `net: container:<service>` entries are taken into account when resolving dependencies, so `docker-compose up <service>` will correctly start all dependencies of `<service>`.
- Problems with authentication when using images from third-party registries have been fixed.
- `docker-compose run` now accepts a `--user` argument to specify a user to run the command as, just like `docker run`.
- The `up`, `stop` and `restart` commands now accept a `--timeout` (or `-t`) argument to specify how long to wait when attempting to gracefully stop containers, just like `docker stop`.
- `docker-compose rm` now accepts `-f` as a shorthand for `--force`, just like `docker rm`.
Thanks, @abesto, @albers, @alunduil, @dnephin, @funkyfuture, @gilclark, @IanVS, @KingsleyKelly, @knutwalker, @thaJeztah and @vmalloc!
1.1.0 (2015-02-25) 1.1.0 (2015-02-25)
------------------ ------------------

View file

@ -23,6 +23,9 @@ RUN set -ex; \
chmod +x /usr/local/bin/docker-$v; \ chmod +x /usr/local/bin/docker-$v; \
done done
# Set the default Docker to be run
RUN ln -s /usr/local/bin/docker-1.3.3 /usr/local/bin/docker
RUN useradd -d /home/user -m -s /bin/bash user RUN useradd -d /home/user -m -s /bin/bash user
WORKDIR /code/ WORKDIR /code/

View file

@ -1,4 +1,4 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from .service import Service # noqa:flake8 from .service import Service # noqa:flake8
__version__ = '1.1.0' __version__ = '1.2.0rc4'

View file

@ -171,6 +171,9 @@ def process_container_options(service_dict, working_dir=None):
if 'volumes' in service_dict: if 'volumes' in service_dict:
service_dict['volumes'] = resolve_host_paths(service_dict['volumes'], working_dir=working_dir) service_dict['volumes'] = resolve_host_paths(service_dict['volumes'], working_dir=working_dir)
if 'build' in service_dict:
service_dict['build'] = resolve_build_path(service_dict['build'], working_dir=working_dir)
return service_dict return service_dict
@ -189,10 +192,29 @@ def merge_service_dicts(base, override):
override.get('volumes'), override.get('volumes'),
) )
for k in ALLOWED_KEYS: if 'image' in override and 'build' in d:
if k not in ['environment', 'volumes']: del d['build']
if k in override:
d[k] = override[k] if 'build' in override and 'image' in d:
del d['image']
list_keys = ['ports', 'expose', 'external_links']
for key in list_keys:
if key in base or key in override:
d[key] = base.get(key, []) + override.get(key, [])
list_or_string_keys = ['dns', 'dns_search']
for key in list_or_string_keys:
if key in base or key in override:
d[key] = to_list(base.get(key)) + to_list(override.get(key))
already_merged_keys = ['environment', 'volumes'] + list_keys + list_or_string_keys
for k in set(ALLOWED_KEYS) - set(already_merged_keys):
if k in override:
d[k] = override[k]
return d return d
@ -306,11 +328,24 @@ def resolve_host_paths(volumes, working_dir=None):
def resolve_host_path(volume, working_dir): def resolve_host_path(volume, working_dir):
container_path, host_path = split_volume(volume) container_path, host_path = split_volume(volume)
if host_path is not None: if host_path is not None:
host_path = os.path.expanduser(host_path)
host_path = os.path.expandvars(host_path)
return "%s:%s" % (expand_path(working_dir, host_path), container_path) return "%s:%s" % (expand_path(working_dir, host_path), container_path)
else: else:
return container_path return container_path
def resolve_build_path(build_path, working_dir=None):
if working_dir is None:
raise Exception("No working_dir passed to resolve_build_path")
_path = expand_path(working_dir, build_path)
if not os.path.exists(_path) or not os.access(_path, os.R_OK):
raise ConfigurationError("build path %s either does not exist or is not accessible." % _path)
else:
return _path
def merge_volumes(base, override): def merge_volumes(base, override):
d = dict_from_volumes(base) d = dict_from_volumes(base)
d.update(dict_from_volumes(override)) d.update(dict_from_volumes(override))
@ -348,6 +383,15 @@ def expand_path(working_dir, path):
return os.path.abspath(os.path.join(working_dir, path)) return os.path.abspath(os.path.join(working_dir, path))
def to_list(value):
if value is None:
return []
elif isinstance(value, six.string_types):
return [value]
else:
return value
def get_service_name_from_net(net_config): def get_service_name_from_net(net_config):
if not net_config: if not net_config:
return return

View file

@ -3,7 +3,6 @@ from __future__ import absolute_import
from collections import namedtuple from collections import namedtuple
import logging import logging
import re import re
import os
from operator import attrgetter from operator import attrgetter
import sys import sys
import six import six
@ -586,8 +585,7 @@ def parse_repository_tag(s):
def build_volume_binding(volume_spec): def build_volume_binding(volume_spec):
internal = {'bind': volume_spec.internal, 'ro': volume_spec.mode == 'ro'} internal = {'bind': volume_spec.internal, 'ro': volume_spec.mode == 'ro'}
external = os.path.expanduser(volume_spec.external) return volume_spec.external, internal
return os.path.abspath(os.path.expandvars(external)), internal
def build_port_bindings(ports): def build_port_bindings(ports):

View file

@ -17,7 +17,7 @@ On a Mac, install with `brew install bash-completion`
Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g. Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g.
curl -L https://raw.githubusercontent.com/docker/compose/1.1.0/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose curl -L https://raw.githubusercontent.com/docker/compose/1.2.0rc4/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
Completion will be available upon next login. Completion will be available upon next login.

View file

@ -20,7 +20,7 @@ First, install Docker version 1.3 or greater:
To install Compose, run the following commands: To install Compose, run the following commands:
curl -L https://github.com/docker/compose/releases/download/1.1.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose curl -L https://github.com/docker/compose/releases/download/1.2.0rc4/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose
Optionally, you can also install [command completion](completion.md) for the Optionally, you can also install [command completion](completion.md) for the

View file

@ -29,8 +29,9 @@ image: a4bc65fd
### build ### build
Path to a directory containing a Dockerfile. This directory is also the Path to a directory containing a Dockerfile. When the value supplied is a
build context that is sent to the Docker daemon. relative path, it is interpreted as relative to the location of the yml file
itself. This directory is also the build context that is sent to the Docker daemon.
Compose will build and tag it with a generated name, and use that image thereafter. Compose will build and tag it with a generated name, and use that image thereafter.

View file

@ -1,8 +1,12 @@
#!/bin/sh #!/bin/bash
set -ex set -ex
mkdir -p `pwd`/dist
chmod 777 `pwd`/dist TAG="docker-compose"
docker build -t docker-compose . docker build -t "$TAG" .
docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint pyinstaller docker-compose -F bin/docker-compose docker run \
mv dist/docker-compose dist/docker-compose-Linux-x86_64 --rm \
docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint dist/docker-compose-Linux-x86_64 docker-compose --version --user=user \
--volume="$(pwd):/code" \
--entrypoint="script/build-linux-inner" \
"$TAG"

10
script/build-linux-inner Executable file
View file

@ -0,0 +1,10 @@
#!/bin/bash
set -ex
mkdir -p `pwd`/dist
chmod 777 `pwd`/dist
pyinstaller -F bin/docker-compose
mv dist/docker-compose dist/docker-compose-Linux-x86_64
dist/docker-compose-Linux-x86_64 --version

18
script/ci Executable file
View file

@ -0,0 +1,18 @@
#!/bin/bash
# This should be run inside a container built from the Dockerfile
# at the root of the repo:
#
# $ TAG="docker-compose:$(git rev-parse --short HEAD)"
# $ docker build -t "$TAG" .
# $ docker run --rm --volume="/var/run/docker.sock:/var/run/docker.sock" --volume="$(pwd)/.git:/code/.git" -e "TAG=$TAG" --entrypoint="script/ci" "$TAG"
set -e
>&2 echo "Validating DCO"
script/validate-dco
export DOCKER_VERSIONS=all
. script/test-versions
>&2 echo "Building Linux binary"
su -c script/build-linux-inner user

View file

@ -4,9 +4,6 @@
set -e set -e
>&2 echo "Validating DCO"
script/validate-dco
>&2 echo "Running lint checks" >&2 echo "Running lint checks"
flake8 compose flake8 compose
@ -18,7 +15,7 @@ fi
for version in $DOCKER_VERSIONS; do for version in $DOCKER_VERSIONS; do
>&2 echo "Running tests against Docker $version" >&2 echo "Running tests against Docker $version"
docker-1.5.0 run \ docker run \
--rm \ --rm \
--privileged \ --privileged \
--volume="/var/lib/docker" \ --volume="/var/lib/docker" \

View file

@ -4,7 +4,7 @@ if [ "$DOCKER_VERSION" == "" ]; then
DOCKER_VERSION="1.5.0" DOCKER_VERSION="1.5.0"
fi fi
ln -s "/usr/local/bin/docker-$DOCKER_VERSION" "/usr/local/bin/docker" ln -fs "/usr/local/bin/docker-$DOCKER_VERSION" "/usr/local/bin/docker"
# If a pidfile is still around (for example after a container restart), # If a pidfile is still around (for example after a container restart),
# delete it so that docker can start. # delete it so that docker can start.

2
tests/fixtures/build-ctx/Dockerfile vendored Normal file
View file

@ -0,0 +1,2 @@
FROM busybox:latest
CMD echo "success"

View file

@ -0,0 +1,2 @@
foo:
build: ../build-ctx/

View file

@ -1,2 +1,2 @@
service: service:
build: tests/fixtures/dockerfile_with_entrypoint build: .

View file

@ -1,2 +1,2 @@
simple: simple:
build: tests/fixtures/simple-dockerfile build: .

View file

@ -123,6 +123,24 @@ class ServiceTest(DockerClientTestCase):
self.assertTrue(path.basename(actual_host_path) == path.basename(host_path), self.assertTrue(path.basename(actual_host_path) == path.basename(host_path),
msg=("Last component differs: %s, %s" % (actual_host_path, host_path))) msg=("Last component differs: %s, %s" % (actual_host_path, host_path)))
@mock.patch.dict(os.environ)
def test_create_container_with_home_and_env_var_in_volume_path(self):
os.environ['VOLUME_NAME'] = 'my-volume'
os.environ['HOME'] = '/tmp/home-dir'
expected_host_path = os.path.join(os.environ['HOME'], os.environ['VOLUME_NAME'])
host_path = '~/${VOLUME_NAME}'
container_path = '/container-path'
service = self.create_service('db', volumes=['%s:%s' % (host_path, container_path)])
container = service.create_container()
service.start_container(container)
actual_host_path = container.get('Volumes')[container_path]
components = actual_host_path.split('/')
self.assertTrue(components[-2:] == ['home-dir', 'my-volume'],
msg="Last two components differ: %s, %s" % (actual_host_path, expected_host_path))
def test_create_container_with_volumes_from(self): def test_create_container_with_volumes_from(self):
volume_service = self.create_service('data') volume_service = self.create_service('data')
volume_container_1 = volume_service.create_container() volume_container_1 = volume_service.create_container()

View file

@ -39,46 +39,150 @@ class ConfigTest(unittest.TestCase):
config.make_service_dict('foo', {'ports': ['8000']}) config.make_service_dict('foo', {'ports': ['8000']})
class MergeTest(unittest.TestCase): class VolumePathTest(unittest.TestCase):
def test_merge_volumes_empty(self): @mock.patch.dict(os.environ)
def test_volume_binding_with_environ(self):
os.environ['VOLUME_PATH'] = '/host/path'
d = config.make_service_dict('foo', {'volumes': ['${VOLUME_PATH}:/container/path']}, working_dir='.')
self.assertEqual(d['volumes'], ['/host/path:/container/path'])
@mock.patch.dict(os.environ)
def test_volume_binding_with_home(self):
os.environ['HOME'] = '/home/user'
d = config.make_service_dict('foo', {'volumes': ['~:/container/path']}, working_dir='.')
self.assertEqual(d['volumes'], ['/home/user:/container/path'])
class MergeVolumesTest(unittest.TestCase):
def test_empty(self):
service_dict = config.merge_service_dicts({}, {}) service_dict = config.merge_service_dicts({}, {})
self.assertNotIn('volumes', service_dict) self.assertNotIn('volumes', service_dict)
def test_merge_volumes_no_override(self): def test_no_override(self):
service_dict = config.merge_service_dicts( service_dict = config.merge_service_dicts(
{'volumes': ['/foo:/code', '/data']}, {'volumes': ['/foo:/code', '/data']},
{}, {},
) )
self.assertEqual(set(service_dict['volumes']), set(['/foo:/code', '/data'])) self.assertEqual(set(service_dict['volumes']), set(['/foo:/code', '/data']))
def test_merge_volumes_no_base(self): def test_no_base(self):
service_dict = config.merge_service_dicts( service_dict = config.merge_service_dicts(
{}, {},
{'volumes': ['/bar:/code']}, {'volumes': ['/bar:/code']},
) )
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code'])) self.assertEqual(set(service_dict['volumes']), set(['/bar:/code']))
def test_merge_volumes_override_explicit_path(self): def test_override_explicit_path(self):
service_dict = config.merge_service_dicts( service_dict = config.merge_service_dicts(
{'volumes': ['/foo:/code', '/data']}, {'volumes': ['/foo:/code', '/data']},
{'volumes': ['/bar:/code']}, {'volumes': ['/bar:/code']},
) )
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data'])) self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data']))
def test_merge_volumes_add_explicit_path(self): def test_add_explicit_path(self):
service_dict = config.merge_service_dicts( service_dict = config.merge_service_dicts(
{'volumes': ['/foo:/code', '/data']}, {'volumes': ['/foo:/code', '/data']},
{'volumes': ['/bar:/code', '/quux:/data']}, {'volumes': ['/bar:/code', '/quux:/data']},
) )
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/quux:/data'])) self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/quux:/data']))
def test_merge_volumes_remove_explicit_path(self): def test_remove_explicit_path(self):
service_dict = config.merge_service_dicts( service_dict = config.merge_service_dicts(
{'volumes': ['/foo:/code', '/quux:/data']}, {'volumes': ['/foo:/code', '/quux:/data']},
{'volumes': ['/bar:/code', '/data']}, {'volumes': ['/bar:/code', '/data']},
) )
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data'])) self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data']))
def test_merge_build_or_image_no_override(self):
self.assertEqual(
config.merge_service_dicts({'build': '.'}, {}),
{'build': '.'},
)
self.assertEqual(
config.merge_service_dicts({'image': 'redis'}, {}),
{'image': 'redis'},
)
def test_merge_build_or_image_override_with_same(self):
self.assertEqual(
config.merge_service_dicts({'build': '.'}, {'build': './web'}),
{'build': './web'},
)
self.assertEqual(
config.merge_service_dicts({'image': 'redis'}, {'image': 'postgres'}),
{'image': 'postgres'},
)
def test_merge_build_or_image_override_with_other(self):
self.assertEqual(
config.merge_service_dicts({'build': '.'}, {'image': 'redis'}),
{'image': 'redis'}
)
self.assertEqual(
config.merge_service_dicts({'image': 'redis'}, {'build': '.'}),
{'build': '.'}
)
class MergeListsTest(unittest.TestCase):
def test_empty(self):
service_dict = config.merge_service_dicts({}, {})
self.assertNotIn('ports', service_dict)
def test_no_override(self):
service_dict = config.merge_service_dicts(
{'ports': ['10:8000', '9000']},
{},
)
self.assertEqual(set(service_dict['ports']), set(['10:8000', '9000']))
def test_no_base(self):
service_dict = config.merge_service_dicts(
{},
{'ports': ['10:8000', '9000']},
)
self.assertEqual(set(service_dict['ports']), set(['10:8000', '9000']))
def test_add_item(self):
service_dict = config.merge_service_dicts(
{'ports': ['10:8000', '9000']},
{'ports': ['20:8000']},
)
self.assertEqual(set(service_dict['ports']), set(['10:8000', '9000', '20:8000']))
class MergeStringsOrListsTest(unittest.TestCase):
def test_no_override(self):
service_dict = config.merge_service_dicts(
{'dns': '8.8.8.8'},
{},
)
self.assertEqual(set(service_dict['dns']), set(['8.8.8.8']))
def test_no_base(self):
service_dict = config.merge_service_dicts(
{},
{'dns': '8.8.8.8'},
)
self.assertEqual(set(service_dict['dns']), set(['8.8.8.8']))
def test_add_string(self):
service_dict = config.merge_service_dicts(
{'dns': ['8.8.8.8']},
{'dns': '9.9.9.9'},
)
self.assertEqual(set(service_dict['dns']), set(['8.8.8.8', '9.9.9.9']))
def test_add_list(self):
service_dict = config.merge_service_dicts(
{'dns': '8.8.8.8'},
{'dns': ['9.9.9.9']},
)
self.assertEqual(set(service_dict['dns']), set(['8.8.8.8', '9.9.9.9']))
class EnvTest(unittest.TestCase): class EnvTest(unittest.TestCase):
def test_parse_environment_as_list(self): def test_parse_environment_as_list(self):
@ -291,3 +395,36 @@ class ExtendsTest(unittest.TestCase):
] ]
self.assertEqual(set(dicts[0]['volumes']), set(paths)) self.assertEqual(set(dicts[0]['volumes']), set(paths))
class BuildPathTest(unittest.TestCase):
def setUp(self):
self.abs_context_path = os.path.join(os.getcwd(), 'tests/fixtures/build-ctx')
def test_nonexistent_path(self):
options = {'build': 'nonexistent.path'}
self.assertRaises(
config.ConfigurationError,
lambda: config.make_service_dict('foo', options, 'tests/fixtures/build-path'),
)
def test_relative_path(self):
relative_build_path = '../build-ctx/'
service_dict = config.make_service_dict(
'relpath',
{'build': relative_build_path},
working_dir='tests/fixtures/build-path'
)
self.assertEquals(service_dict['build'], self.abs_context_path)
def test_absolute_path(self):
service_dict = config.make_service_dict(
'abspath',
{'build': self.abs_context_path},
working_dir='tests/fixtures/build-path'
)
self.assertEquals(service_dict['build'], self.abs_context_path)
def test_from_file(self):
service_dict = config.load('tests/fixtures/build-path/docker-compose.yml')
self.assertEquals(service_dict, [{'name': 'foo', 'build': self.abs_context_path}])

View file

@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from __future__ import absolute_import from __future__ import absolute_import
import os
from .. import unittest from .. import unittest
import mock import mock
@ -301,18 +300,3 @@ class ServiceVolumesTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
binding, binding,
('/outside', dict(bind='/inside', ro=False))) ('/outside', dict(bind='/inside', ro=False)))
@mock.patch.dict(os.environ)
def test_build_volume_binding_with_environ(self):
os.environ['VOLUME_PATH'] = '/opt'
binding = build_volume_binding(parse_volume_spec('${VOLUME_PATH}:/opt'))
self.assertEqual(binding, ('/opt', dict(bind='/opt', ro=False)))
@mock.patch.dict(os.environ)
def test_building_volume_binding_with_home(self):
os.environ['HOME'] = '/home/user'
binding = build_volume_binding(parse_volume_spec('~:/home/user'))
self.assertEqual(
binding,
('/home/user', dict(bind='/home/user', ro=False)))