Repository: ofw/curlify

Issue:
deprecation warning with pip 22.3 missing pyproject.toml
```
DEPRECATION: curlify is being installed using the legacy 'setup.py install' method, 
because it does not have a 'pyproject.toml' and the 'wheel' package is not installed.
pip 23.1 will enforce this behaviour change. A possible replacement is to enable the '--use-pep517' option.
Discussion can be found at https://github.com/pypa/pip/issues/8559
````
https://github.com/pypa/pip/issues/8559

Discussion:

Solution patch:
diff --git a/CHANGES.md b/CHANGES.md
deleted file mode 100644
index e69de29..0000000
diff --git a/MANIFEST.in b/MANIFEST.in
index 3ed58da..787c21a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,3 @@
-
 include CHANGES.md
 include README.md
 include LICENSE
diff --git a/README.md b/README.md
index 5d83867..c32e775 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,38 @@
-# Curlify - convert python requests request object to cURL command
+# Curlify - convert Python Requests request object to cURL command
 
 ## Installation
+
 ```sh
 pip install curlify
 ```
 
 ## Changes
 
-### v.2.2.0
+### v3.0.0
+   * Remove Python 2 compatibility
+   * Add `pretty` parameter to enable generating a multi-line command
+   * Don't add `-X` when it's unnecessary
+   * Correctly send empty headers
+   * Don't generate `--data` that would read a file
+   * Don't sort headers, their order is deterministic on Python 3.7+
+
+### v2.2.0
    * Fixed shell quotes. Fixed posting CSV file. Thanks to @leNEKO
 
-### v.2.1.1
+### v2.1.1
    * Add `--insecure` flag if `verify` parameter is not `True`
 
-### v.2.1.0
+### v2.1.0
    * Fixed body rendering when using `json` param to request function.
 
-### v.2.0.1
+### v2.0.1
    * Added `compressed` parameter to `to_curl` function, if it is needed to add `--compressed` option to generated cURL command.
 
-### v.2.0
+### v2.0
    * Skip `-d` option if request body is empty https://github.com/ofw/curlify/issues/6
    * Minor changes to header sorting
 
-### v.1.2
+### v1.2
    * Order of headers is deterministic (thanks to @tomviner)
 
 ## Example
diff --git a/curlify.py b/curlify.py
index 6b5d902..7a5fc5f 100644
--- a/curlify.py
+++ b/curlify.py
@@ -1,48 +1,53 @@
-# coding: utf-8
-import sys
+from shlex import quote
 
-if sys.version_info.major >= 3:
-    from shlex import quote
-else:
-    from pipes import quote
 
-
-def to_curl(request, compressed=False, verify=True):
+def to_curl(request, compressed=False, verify=True, pretty=False):
     """
-    Returns string with curl command by provided request object
+    Returns a string with a curl command that makes the same HTTP request as
+    the provided request object.
 
     Parameters
     ----------
     compressed : bool
-        If `True` then `--compressed` argument will be added to result
+        If `True` then a `--compressed` argument will be added to the result
+    verify : bool
+        If `False` then a `--insecure` argument will be added to the result,
+        disabling TLS certificate verification
     """
-    parts = [
-        ('curl', None),
-        ('-X', request.method),
-    ]
+    command = []
+
+    inferred_method = 'GET'
+    if request.body is not None:
+        inferred_method = 'POST'
+    if request.method != inferred_method:
+        command.append('-X ' + quote(request.method))
 
-    for k, v in sorted(request.headers.items()):
-        parts += [('-H', '{0}: {1}'.format(k, v))]
+    for k, v in request.headers.items():
+        if v:
+            command.append('-H ' + quote('{0}: {1}'.format(k, v)))
+        else:
+            # -H 'Accept:' disables sending the Accept header, use semicolon to send
+            # empty header
+            command.append('-H ' + quote('{0};'.format(k)))
 
     if request.body:
         body = request.body
         if isinstance(body, bytes):
             body = body.decode('utf-8')
-        parts += [('-d', body)]
+        data_type = '-d'
+        if body.startswith('@'):  # -d @filename causes curl to read from file
+            data_type = '--data-raw'
+        command.append(data_type + ' ' + quote(body))
 
     if compressed:
-        parts += [('--compressed', None)]
+        command.append('--compressed')
 
     if not verify:
-        parts += [('--insecure', None)]
+        command.append('--insecure')
 
-    parts += [(None, request.url)]
-
-    flat_parts = []
-    for k, v in parts:
-        if k:
-            flat_parts.append(quote(k))
-        if v:
-            flat_parts.append(quote(v))
+    command.append(quote(request.url))
 
-    return ' '.join(flat_parts)
+    joiner = ' '
+    if pretty and len(command) > 3:
+        joiner = ' \\\n  '
+    return 'curl ' + joiner.join(command)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d09ecc1
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,42 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "curlify"
+version = "3.0.0"
+authors = [
+  { name="Egor Orlov", email="oeegor@gmail.com" },
+]
+license = {text = "MIT License"}
+description = "Convert Requests request objects to curl commands."
+readme = "README.md"
+requires-python = ">=3.7"
+classifiers = [
+  "Environment :: Web Environment",
+  "Intended Audience :: Developers",
+  "License :: OSI Approved :: MIT License",
+  "Operating System :: OS Independent",
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3 :: Only",
+  "Programming Language :: Python :: 3.7",
+  "Programming Language :: Python :: 3.8",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Topic :: Internet :: WWW/HTTP",
+  "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+]
+dependencies = [
+  "requests",
+]
+
+[project.urls]
+"Homepage" = "https://github.com/oeegor/curlify"
+"Bug Tracker" = "https://github.com/oeegor/curlify/issues"
+
+[tool.setuptools]
+py-modules = [
+  "curlify",
+]
diff --git a/pytest.ini b/pytest.ini
index 16b4040..3720f93 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -2,6 +2,3 @@
 addopts = --pyargs
 norecursedirs = env venv
 python_files= *_test.py *_itest.py
-pep8ignore = * E401,E402,E122,E126,E127,E128,E124,E241,E501
-pep8maxlinelength = 120
-
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 16da08f..0000000
--- a/setup.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# coding: utf-8
-
-import os
-from setuptools import setup, Command
-
-
-class CleanCommand(Command):
-    """Custom clean command to tidy up the project root."""
-    user_options = []
-
-    def initialize_options(self):
-        pass
-
-    def finalize_options(self):
-        pass
-
-    def run(self):
-        os.system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info')
-
-
-# allow setup.py to be run from any path
-os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
-
-
-setup(
-    name='curlify',
-    version='2.2.1',
-    py_modules=[
-        'curlify',
-    ],
-    include_package_data=True,
-    install_requires=[
-        'requests',
-    ],
-    license='MIT License',
-    description='Library to convert python requests object to curl command.',
-    author='Egor Orlov',
-    author_email='oeegor@gmail.com',
-    platforms='any',
-    url='https://github.com/oeegor/curlify',
-    classifiers=[
-        'Environment :: Web Environment',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 2.7',
-        'Programming Language :: Python :: 3.5',
-        'Topic :: Internet :: WWW/HTTP',
-        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
-    ],
-    cmdclass={
-        'clean': CleanCommand,
-    },
-)


Test patch:
diff --git a/curlify_test.py b/curlify_test.py
index 16e71b9..757c51d 100644
--- a/curlify_test.py
+++ b/curlify_test.py
@@ -1,4 +1,3 @@
-# coding: utf-8
 import curlify
 import re
 import requests
@@ -11,11 +10,11 @@ def test_empty_data():
     )
     assert curlify.to_curl(r.request) == (
         "curl -X POST "
-        "-H 'Accept: */*' "
+        "-H 'user-agent: mytest' "
         "-H 'Accept-Encoding: gzip, deflate' "
+        "-H 'Accept: */*' "
         "-H 'Connection: keep-alive' "
         "-H 'Content-Length: 0' "
-        "-H 'user-agent: mytest' "
         "http://google.ru/"
     )
 
@@ -29,13 +28,13 @@ def test_ok():
     )
     assert curlify.to_curl(r.request) == (
         "curl -X GET "
-        "-H 'Accept: */*' "
+        "-H 'user-agent: mytest' "
         "-H 'Accept-Encoding: gzip, deflate' "
+        "-H 'Accept: */*' "
         "-H 'Connection: keep-alive' "
+        "-H 'Cookie: foo=bar' "
         "-H 'Content-Length: 3' "
         "-H 'Content-Type: application/x-www-form-urlencoded' "
-        "-H 'Cookie: foo=bar' "
-        "-H 'user-agent: mytest' "
         "-d a=b http://google.ru/"
     )
 
@@ -47,7 +46,7 @@ def test_prepare_request():
     )
 
     assert curlify.to_curl(request.prepare()) == (
-        "curl -X GET "
+        "curl "
         "-H 'user-agent: UA' "
         "http://google.ru/"
     )
@@ -59,7 +58,7 @@ def test_compressed():
         headers={"user-agent": "UA"},
     )
     assert curlify.to_curl(request.prepare(), compressed=True) == (
-        "curl -X GET -H 'user-agent: UA' --compressed http://google.ru/"
+        "curl -H 'user-agent: UA' --compressed http://google.ru/"
     )
 
 
@@ -69,7 +68,7 @@ def test_verify():
         headers={"user-agent": "UA"},
     )
     assert curlify.to_curl(request.prepare(), verify=False) == (
-        "curl -X GET -H 'user-agent: UA' --insecure http://google.ru/"
+        "curl -H 'user-agent: UA' --insecure http://google.ru/"
     )
 
 
@@ -81,7 +80,7 @@ def test_post_json():
     curlified = curlify.to_curl(r.prepare())
 
     assert curlified == (
-        "curl -X POST -H 'Content-Length: 14' "
+        "curl -H 'Content-Length: 14' "
         "-H 'Content-Type: application/json' "
         "-d '{\"foo\": \"bar\"}' https://httpbin.org/post"
     )
@@ -99,9 +98,10 @@ def test_post_csv_file():
     boundary = re.search(r'boundary=(\w+)', curlified).group(1)
 
     expected = (
-        'curl -X POST -H \'Content-Length: 519\''
-        f' -H \'Content-Type: multipart/form-data; boundary={boundary}\''
+        'curl'
         ' -H \'User-agent: UA\''
+        ' -H \'Content-Length: 519\''
+        f' -H \'Content-Type: multipart/form-data; boundary={boundary}\''
         f' -d \'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="data.csv"\r\n\r\n'
         '"Id";"Title";"Content"\n'
         '1;"Simple Test";"Ici un test d\'"\'"\'échappement de simple quote"\n'
@@ -111,3 +111,44 @@ def test_post_csv_file():
     )
 
     assert curlified == expected
+
+
+def test_data_with_at():
+    request = requests.Request(
+        'GET', "http://google.ru",
+        data='@example.com'
+    )
+    assert curlify.to_curl(request.prepare()) == (
+        "curl -X GET -H 'Content-Length: 12' --data-raw @example.com http://google.ru/"
+    )
+
+
+def test_empty_header():
+    request = requests.Request(
+        'POST', "http://google.ru",
+        headers={'User-agent': ''}
+    )
+    assert curlify.to_curl(request.prepare()) == (
+        "curl -X POST -H 'User-agent;' -H 'Content-Length: 0' http://google.ru/"
+    )
+
+
+def test_pretty():
+    request = requests.Request(
+        'GET', "http://google.ru",
+        data={'foo': 'bar'}
+    )
+    assert curlify.to_curl(request.prepare(), pretty=True) == '''curl -X GET \\
+  -H 'Content-Length: 7' \\
+  -H 'Content-Type: application/x-www-form-urlencoded' \\
+  -d foo=bar \\
+  http://google.ru/'''
+
+
+def test_short():
+    request = requests.Request(
+        'GET', "http://google.ru",
+    )
+    assert curlify.to_curl(request.prepare(), pretty=True) == (
+        "curl http://google.ru/"
+    )