Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
python-poetry
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
open
python-poetry
Commits
4835c591
Commit
4835c591
authored
Apr 07, 2021
by
Arun Babu Neelicattu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
pep610: handle pure/plat lib differences cleanly
parent
a12d1428
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
98 additions
and
64 deletions
+98
-64
poetry/installation/executor.py
+25
-20
poetry/installation/pip_installer.py
+1
-4
poetry/utils/env.py
+38
-14
tests/installation/test_executor.py
+15
-10
tests/installation/test_pip_installer.py
+4
-7
tests/masonry/builders/test_editable_builder.py
+15
-9
No files found.
poetry/installation/executor.py
View file @
4835c591
...
@@ -714,6 +714,19 @@ class Executor:
...
@@ -714,6 +714,19 @@ class Executor:
def
_should_write_operation
(
self
,
operation
:
Operation
)
->
bool
:
def
_should_write_operation
(
self
,
operation
:
Operation
)
->
bool
:
return
not
operation
.
skipped
or
self
.
_dry_run
or
self
.
_verbose
return
not
operation
.
skipped
or
self
.
_dry_run
or
self
.
_verbose
@staticmethod
def
_package_dist_info_path
(
package
:
"Package"
)
->
Path
:
from
poetry.core.masonry.utils.helpers
import
escape_name
from
poetry.core.masonry.utils.helpers
import
escape_version
return
Path
(
f
"{escape_name(package.pretty_name)}-{escape_version(package.version.text)}.dist-info"
)
@classmethod
def
_direct_url_json_path
(
cls
,
package
:
"Package"
)
->
Path
:
return
cls
.
_package_dist_info_path
(
package
)
/
"direct_url.json"
def
_save_url_reference
(
self
,
operation
:
"OperationTypes"
)
->
None
:
def
_save_url_reference
(
self
,
operation
:
"OperationTypes"
)
->
None
:
"""
"""
Create and store a PEP-610 `direct_url.json` file, if needed.
Create and store a PEP-610 `direct_url.json` file, if needed.
...
@@ -721,9 +734,6 @@ class Executor:
...
@@ -721,9 +734,6 @@ class Executor:
if
operation
.
job_type
not
in
{
"install"
,
"update"
}:
if
operation
.
job_type
not
in
{
"install"
,
"update"
}:
return
return
from
poetry.core.masonry.utils.helpers
import
escape_name
from
poetry.core.masonry.utils.helpers
import
escape_version
package
=
operation
.
package
package
=
operation
.
package
if
not
package
.
source_url
:
if
not
package
.
source_url
:
...
@@ -732,14 +742,10 @@ class Executor:
...
@@ -732,14 +742,10 @@ class Executor:
# distribution.
# distribution.
# That's not what we want so we remove the direct_url.json file,
# That's not what we want so we remove the direct_url.json file,
# if it exists.
# if it exists.
dist_info
=
self
.
_env
.
site_packages
.
path
.
joinpath
(
for
direct_url
in
self
.
_env
.
site_packages
.
find
(
"{}-{}.dist-info"
.
format
(
self
.
_direct_url_json_path
(
package
),
True
escape_name
(
package
.
pretty_name
),
):
escape_version
(
package
.
version
.
text
),
direct_url
.
unlink
()
)
)
if
dist_info
.
exists
()
and
dist_info
.
joinpath
(
"direct_url.json"
)
.
exists
():
dist_info
.
joinpath
(
"direct_url.json"
)
.
unlink
()
return
return
...
@@ -755,16 +761,15 @@ class Executor:
...
@@ -755,16 +761,15 @@ class Executor:
url_reference
=
self
.
_create_file_url_reference
(
package
)
url_reference
=
self
.
_create_file_url_reference
(
package
)
if
url_reference
:
if
url_reference
:
dist_info
=
self
.
_env
.
site_packages
.
path
.
joinpath
(
for
path
in
self
.
_env
.
site_packages
.
find
(
"{}-{}.dist-info"
.
format
(
self
.
_package_dist_info_path
(
package
),
writable_only
=
True
escape_name
(
package
.
name
),
escape_version
(
package
.
version
.
text
)
):
)
self
.
_env
.
site_packages
.
write_text
(
)
path
/
"direct_url.json"
,
json
.
dumps
(
url_reference
),
if
dist_info
.
exists
():
encoding
=
"utf-8"
,
dist_info
.
joinpath
(
"direct_url.json"
)
.
write_text
(
json
.
dumps
(
url_reference
),
encoding
=
"utf-8"
)
)
break
def
_create_git_url_reference
(
def
_create_git_url_reference
(
self
,
package
:
"Package"
self
,
package
:
"Package"
...
...
poetry/installation/pip_installer.py
View file @
4835c591
...
@@ -117,10 +117,7 @@ class PipInstaller(BaseInstaller):
...
@@ -117,10 +117,7 @@ class PipInstaller(BaseInstaller):
raise
raise
# This is a workaround for https://github.com/pypa/pip/issues/4176
# This is a workaround for https://github.com/pypa/pip/issues/4176
nspkg_pth_file
=
self
.
_env
.
site_packages
.
path
/
"{}-nspkg.pth"
.
format
(
for
nspkg_pth_file
in
self
.
_env
.
site_packages
.
find
(
f
"{package.name}-nspkg.pth"
):
package
.
name
)
if
nspkg_pth_file
.
exists
():
nspkg_pth_file
.
unlink
()
nspkg_pth_file
.
unlink
()
# If we have a VCS package, remove its source directory
# If we have a VCS package, remove its source directory
...
...
poetry/utils/env.py
View file @
4835c591
...
@@ -152,17 +152,34 @@ print(json.dumps(sysconfig.get_paths()))
...
@@ -152,17 +152,34 @@ print(json.dumps(sysconfig.get_paths()))
class
SitePackages
:
class
SitePackages
:
def
__init__
(
def
__init__
(
self
,
path
:
Path
,
fallbacks
:
List
[
Path
]
=
None
,
skip_write_checks
:
bool
=
False
self
,
purelib
:
Path
,
platlib
:
Optional
[
Path
]
=
None
,
fallbacks
:
List
[
Path
]
=
None
,
skip_write_checks
:
bool
=
False
,
)
->
None
:
)
->
None
:
self
.
_path
=
path
self
.
_purelib
=
purelib
self
.
_platlib
=
platlib
or
purelib
if
platlib
and
platlib
.
resolve
()
==
purelib
.
resolve
():
self
.
_platlib
=
purelib
self
.
_fallbacks
=
fallbacks
or
[]
self
.
_fallbacks
=
fallbacks
or
[]
self
.
_skip_write_checks
=
skip_write_checks
self
.
_skip_write_checks
=
skip_write_checks
self
.
_candidates
=
[
self
.
_path
]
+
self
.
_fallbacks
self
.
_candidates
=
list
({
self
.
_purelib
,
self
.
_platlib
})
+
self
.
_fallbacks
self
.
_writable_candidates
=
None
if
not
skip_write_checks
else
self
.
_candidates
self
.
_writable_candidates
=
None
if
not
skip_write_checks
else
self
.
_candidates
@property
@property
def
path
(
self
)
->
Path
:
def
path
(
self
)
->
Path
:
return
self
.
_path
return
self
.
_purelib
@property
def
purelib
(
self
)
->
Path
:
return
self
.
_purelib
@property
def
platlib
(
self
)
->
Path
:
return
self
.
_platlib
@property
@property
def
candidates
(
self
)
->
List
[
Path
]:
def
candidates
(
self
)
->
List
[
Path
]:
...
@@ -200,12 +217,16 @@ class SitePackages:
...
@@ -200,12 +217,16 @@ class SitePackages:
return
[
candidate
/
path
for
candidate
in
candidates
if
candidate
]
return
[
candidate
/
path
for
candidate
in
candidates
if
candidate
]
def
_path_method_wrapper
(
def
_path_method_wrapper
(
self
,
path
:
Path
,
method
:
str
,
*
args
:
Any
,
**
kwargs
:
Any
self
,
path
:
Union
[
str
,
Path
],
method
:
str
,
*
args
:
Any
,
return_first
:
bool
=
True
,
writable_only
:
bool
=
False
,
**
kwargs
:
Any
,
)
->
Union
[
Tuple
[
Path
,
Any
],
List
[
Tuple
[
Path
,
Any
]]]:
)
->
Union
[
Tuple
[
Path
,
Any
],
List
[
Tuple
[
Path
,
Any
]]]:
if
isinstance
(
path
,
str
):
# TODO: Move to parameters after dropping Python 2.7
path
=
Path
(
path
)
return_first
=
kwargs
.
pop
(
"return_first"
,
True
)
writable_only
=
kwargs
.
pop
(
"writable_only"
,
False
)
candidates
=
self
.
make_candidates
(
path
,
writable_only
=
writable_only
)
candidates
=
self
.
make_candidates
(
path
,
writable_only
=
writable_only
)
...
@@ -234,19 +255,19 @@ class SitePackages:
...
@@ -234,19 +255,19 @@ class SitePackages:
raise
OSError
(
"Unable to access any of {}"
.
format
(
paths_csv
(
candidates
)))
raise
OSError
(
"Unable to access any of {}"
.
format
(
paths_csv
(
candidates
)))
def
write_text
(
self
,
path
:
Path
,
*
args
:
Any
,
**
kwargs
:
Any
)
->
Path
:
def
write_text
(
self
,
path
:
Union
[
str
,
Path
]
,
*
args
:
Any
,
**
kwargs
:
Any
)
->
Path
:
return
self
.
_path_method_wrapper
(
path
,
"write_text"
,
*
args
,
**
kwargs
)[
0
]
return
self
.
_path_method_wrapper
(
path
,
"write_text"
,
*
args
,
**
kwargs
)[
0
]
def
mkdir
(
self
,
path
:
Path
,
*
args
:
Any
,
**
kwargs
:
Any
)
->
Path
:
def
mkdir
(
self
,
path
:
Union
[
str
,
Path
]
,
*
args
:
Any
,
**
kwargs
:
Any
)
->
Path
:
return
self
.
_path_method_wrapper
(
path
,
"mkdir"
,
*
args
,
**
kwargs
)[
0
]
return
self
.
_path_method_wrapper
(
path
,
"mkdir"
,
*
args
,
**
kwargs
)[
0
]
def
exists
(
self
,
path
:
Path
)
->
bool
:
def
exists
(
self
,
path
:
Union
[
str
,
Path
]
)
->
bool
:
return
any
(
return
any
(
value
[
-
1
]
value
[
-
1
]
for
value
in
self
.
_path_method_wrapper
(
path
,
"exists"
,
return_first
=
False
)
for
value
in
self
.
_path_method_wrapper
(
path
,
"exists"
,
return_first
=
False
)
)
)
def
find
(
self
,
path
:
Path
,
writable_only
:
bool
=
False
)
->
List
[
Path
]:
def
find
(
self
,
path
:
Union
[
str
,
Path
]
,
writable_only
:
bool
=
False
)
->
List
[
Path
]:
return
[
return
[
value
[
0
]
value
[
0
]
for
value
in
self
.
_path_method_wrapper
(
for
value
in
self
.
_path_method_wrapper
(
...
@@ -990,7 +1011,10 @@ class Env:
...
@@ -990,7 +1011,10 @@ class Env:
# we disable write checks if no user site exist
# we disable write checks if no user site exist
fallbacks
=
[
self
.
usersite
]
if
self
.
usersite
else
[]
fallbacks
=
[
self
.
usersite
]
if
self
.
usersite
else
[]
self
.
_site_packages
=
SitePackages
(
self
.
_site_packages
=
SitePackages
(
self
.
purelib
,
fallbacks
,
skip_write_checks
=
False
if
fallbacks
else
True
self
.
purelib
,
self
.
platlib
,
fallbacks
,
skip_write_checks
=
False
if
fallbacks
else
True
,
)
)
return
self
.
_site_packages
return
self
.
_site_packages
...
...
tests/installation/test_executor.py
View file @
4835c591
...
@@ -280,9 +280,10 @@ def test_executor_should_write_pep610_url_references_for_files(
...
@@ -280,9 +280,10 @@ def test_executor_should_write_pep610_url_references_for_files(
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
.
execute
([
Install
(
package
)])
executor
.
execute
([
Install
(
package
)])
dist_info
=
tmp_venv
.
site_packages
.
path
.
joinpath
(
"demo-0.1.0.dist-info"
)
dist_info
=
"demo-0.1.0.dist-info"
assert
dist_info
.
exists
(
)
assert
tmp_venv
.
site_packages
.
exists
(
dist_info
)
dist_info
=
tmp_venv
.
site_packages
.
find
(
dist_info
)[
0
]
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
assert
direct_url_file
.
exists
()
assert
direct_url_file
.
exists
()
...
@@ -303,9 +304,10 @@ def test_executor_should_write_pep610_url_references_for_directories(
...
@@ -303,9 +304,10 @@ def test_executor_should_write_pep610_url_references_for_directories(
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
.
execute
([
Install
(
package
)])
executor
.
execute
([
Install
(
package
)])
dist_info
=
tmp_venv
.
site_packages
.
path
.
joinpath
(
"simple_project-1.2.3.dist-info"
)
dist_info
=
"simple_project-1.2.3.dist-info"
assert
dist_info
.
exists
(
)
assert
tmp_venv
.
site_packages
.
exists
(
dist_info
)
dist_info
=
tmp_venv
.
site_packages
.
find
(
dist_info
)[
0
]
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
assert
direct_url_file
.
exists
()
assert
direct_url_file
.
exists
()
...
@@ -330,9 +332,10 @@ def test_executor_should_write_pep610_url_references_for_editable_directories(
...
@@ -330,9 +332,10 @@ def test_executor_should_write_pep610_url_references_for_editable_directories(
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
.
execute
([
Install
(
package
)])
executor
.
execute
([
Install
(
package
)])
dist_info
=
tmp_venv
.
site_packages
.
path
.
joinpath
(
"simple_project-1.2.3.dist-info"
)
dist_info
_dir
=
"simple_project-1.2.3.dist-info"
assert
dist_info
.
exists
(
)
assert
tmp_venv
.
site_packages
.
exists
(
dist_info_dir
)
dist_info
=
tmp_venv
.
site_packages
.
find
(
dist_info_dir
)[
0
]
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
assert
direct_url_file
.
exists
()
assert
direct_url_file
.
exists
()
...
@@ -355,9 +358,10 @@ def test_executor_should_write_pep610_url_references_for_urls(
...
@@ -355,9 +358,10 @@ def test_executor_should_write_pep610_url_references_for_urls(
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
.
execute
([
Install
(
package
)])
executor
.
execute
([
Install
(
package
)])
dist_info
=
tmp_venv
.
site_packages
.
path
.
joinpath
(
"demo-0.1.0.dist-info"
)
dist_info
=
"demo-0.1.0.dist-info"
assert
dist_info
.
exists
(
)
assert
tmp_venv
.
site_packages
.
exists
(
dist_info
)
dist_info
=
tmp_venv
.
site_packages
.
find
(
dist_info
)[
0
]
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
assert
direct_url_file
.
exists
()
assert
direct_url_file
.
exists
()
...
@@ -385,9 +389,10 @@ def test_executor_should_write_pep610_url_references_for_git(
...
@@ -385,9 +389,10 @@ def test_executor_should_write_pep610_url_references_for_git(
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
=
Executor
(
tmp_venv
,
pool
,
config
,
io
)
executor
.
execute
([
Install
(
package
)])
executor
.
execute
([
Install
(
package
)])
dist_info
=
tmp_venv
.
site_packages
.
path
.
joinpath
(
"demo-0.1.2.dist-info"
)
dist_info
=
"demo-0.1.2.dist-info"
assert
dist_info
.
exists
(
)
assert
tmp_venv
.
site_packages
.
exists
(
dist_info
)
dist_info
=
tmp_venv
.
site_packages
.
find
(
dist_info
)[
0
]
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
direct_url_file
=
dist_info
.
joinpath
(
"direct_url.json"
)
assert
direct_url_file
.
exists
()
assert
direct_url_file
.
exists
()
...
...
tests/installation/test_pip_installer.py
View file @
4835c591
import
re
import
shutil
import
shutil
from
pathlib
import
Path
from
pathlib
import
Path
...
@@ -190,11 +191,6 @@ def test_uninstall_git_package_nspkg_pth_cleanup(mocker, tmp_venv, pool):
...
@@ -190,11 +191,6 @@ def test_uninstall_git_package_nspkg_pth_cleanup(mocker, tmp_venv, pool):
source_reference
=
"master"
,
source_reference
=
"master"
,
)
)
# we do this here because the virtual env might not be usable if failure case is triggered
pth_file_candidate
=
tmp_venv
.
site_packages
.
path
/
"{}-nspkg.pth"
.
format
(
package
.
name
)
# in order to reproduce the scenario where the git source is removed prior to proper
# in order to reproduce the scenario where the git source is removed prior to proper
# clean up of nspkg.pth file, we need to make sure the fixture is copied and not
# clean up of nspkg.pth file, we need to make sure the fixture is copied and not
# symlinked into the git src directory
# symlinked into the git src directory
...
@@ -213,8 +209,9 @@ def test_uninstall_git_package_nspkg_pth_cleanup(mocker, tmp_venv, pool):
...
@@ -213,8 +209,9 @@ def test_uninstall_git_package_nspkg_pth_cleanup(mocker, tmp_venv, pool):
installer
.
install
(
package
)
installer
.
install
(
package
)
installer
.
remove
(
package
)
installer
.
remove
(
package
)
assert
not
Path
(
pth_file_candidate
)
.
exists
()
pth_file
=
f
"{package.name}-nspkg.pth"
assert
not
tmp_venv
.
site_packages
.
exists
(
pth_file
)
# any command in the virtual environment should trigger the error message
# any command in the virtual environment should trigger the error message
output
=
tmp_venv
.
run
(
"python"
,
"-m"
,
"site"
)
output
=
tmp_venv
.
run
(
"python"
,
"-m"
,
"site"
)
assert
"Error processing line 1 of {}"
.
format
(
pth_file_candidate
)
not
in
output
assert
not
re
.
match
(
rf
"Error processing line 1 of .*{pth_file}"
,
output
)
tests/masonry/builders/test_editable_builder.py
View file @
4835c591
...
@@ -78,16 +78,18 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_
...
@@ -78,16 +78,18 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_
builder
.
build
()
builder
.
build
()
assert
tmp_venv
.
_bin_dir
.
joinpath
(
"foo"
)
.
exists
()
assert
tmp_venv
.
_bin_dir
.
joinpath
(
"foo"
)
.
exists
()
assert
tmp_venv
.
site_packages
.
path
.
joinpath
(
"simple_project.pth"
)
.
exists
()
pth_file
=
"simple_project.pth"
assert
tmp_venv
.
site_packages
.
exists
(
pth_file
)
assert
(
assert
(
simple_poetry
.
file
.
parent
.
resolve
()
.
as_posix
()
simple_poetry
.
file
.
parent
.
resolve
()
.
as_posix
()
==
tmp_venv
.
site_packages
.
path
.
joinpath
(
"simple_project.pth"
)
==
tmp_venv
.
site_packages
.
find
(
pth_file
)[
0
]
.
read_text
()
.
strip
(
os
.
linesep
)
.
read_text
()
.
strip
(
os
.
linesep
)
)
)
dist_info
=
tmp_venv
.
site_packages
.
path
.
joinpath
(
"simple_project-1.2.3.dist-info"
)
dist_info
=
"simple_project-1.2.3.dist-info"
assert
dist_info
.
exists
()
assert
tmp_venv
.
site_packages
.
exists
(
dist_info
)
dist_info
=
tmp_venv
.
site_packages
.
find
(
dist_info
)[
0
]
assert
dist_info
.
joinpath
(
"INSTALLER"
)
.
exists
()
assert
dist_info
.
joinpath
(
"INSTALLER"
)
.
exists
()
assert
dist_info
.
joinpath
(
"METADATA"
)
.
exists
()
assert
dist_info
.
joinpath
(
"METADATA"
)
.
exists
()
assert
dist_info
.
joinpath
(
"RECORD"
)
.
exists
()
assert
dist_info
.
joinpath
(
"RECORD"
)
.
exists
()
...
@@ -134,7 +136,9 @@ My Package
...
@@ -134,7 +136,9 @@ My Package
assert
metadata
==
dist_info
.
joinpath
(
"METADATA"
)
.
read_text
(
encoding
=
"utf-8"
)
assert
metadata
==
dist_info
.
joinpath
(
"METADATA"
)
.
read_text
(
encoding
=
"utf-8"
)
records
=
dist_info
.
joinpath
(
"RECORD"
)
.
read_text
()
records
=
dist_info
.
joinpath
(
"RECORD"
)
.
read_text
()
assert
str
(
tmp_venv
.
site_packages
.
path
.
joinpath
(
"simple_project.pth"
))
in
records
pth_file
=
"simple_project.pth"
assert
tmp_venv
.
site_packages
.
exists
(
pth_file
)
assert
str
(
tmp_venv
.
site_packages
.
find
(
pth_file
)[
0
])
in
records
assert
str
(
tmp_venv
.
_bin_dir
.
joinpath
(
"foo"
))
in
records
assert
str
(
tmp_venv
.
_bin_dir
.
joinpath
(
"foo"
))
in
records
assert
str
(
tmp_venv
.
_bin_dir
.
joinpath
(
"baz"
))
in
records
assert
str
(
tmp_venv
.
_bin_dir
.
joinpath
(
"baz"
))
in
records
assert
str
(
dist_info
.
joinpath
(
"METADATA"
))
in
records
assert
str
(
dist_info
.
joinpath
(
"METADATA"
))
in
records
...
@@ -201,8 +205,10 @@ def test_builder_installs_proper_files_when_packages_configured(
...
@@ -201,8 +205,10 @@ def test_builder_installs_proper_files_when_packages_configured(
builder
=
EditableBuilder
(
project_with_include
,
tmp_venv
,
NullIO
())
builder
=
EditableBuilder
(
project_with_include
,
tmp_venv
,
NullIO
())
builder
.
build
()
builder
.
build
()
pth_file
=
tmp_venv
.
site_packages
.
path
.
joinpath
(
"with_include.pth"
)
pth_file
=
"with_include.pth"
assert
pth_file
.
is_file
()
assert
tmp_venv
.
site_packages
.
exists
(
pth_file
)
pth_file
=
tmp_venv
.
site_packages
.
find
(
pth_file
)[
0
]
paths
=
set
()
paths
=
set
()
with
pth_file
.
open
()
as
f
:
with
pth_file
.
open
()
as
f
:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment