Skip to content

Commit 80ee9b3

Browse files
authored
Merge branch 'main' into dynamic=chunksize
2 parents d73bf2b + 2ccadeb commit 80ee9b3

File tree

11 files changed

+275
-37
lines changed

11 files changed

+275
-37
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ repos:
1111
- id: black
1212
exclude: ^docs/
1313
- repo: https://github.com/pycqa/flake8
14-
rev: '4.0.1'
14+
rev: '7.3.0'
1515
hooks:
1616
- id: flake8
1717
exclude: tests/|^docs/|__init__.py

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
s3fs
2+
====
3+
4+
[|Build Status|](https://github.com/fsspec/s3fs/actions)
5+
[|Documentation|](https://s3fs.readthedocs.io/en/latest/?badge=latest)
6+
7+
S3FS builds on [aiobotocore](https://aiobotocore.readthedocs.io/en/latest/)
8+
to provide a convenient Python filesystem interface for S3.
9+
10+
11+
Support
12+
-------
13+
14+
Work on this repository is supported in part by:
15+
16+
"Anaconda, Inc. - Advancing AI through open source."
17+
18+
<a href="https://anaconda.com/"><img src="https://camo.githubusercontent.com/b8555ef2222598ed37ce38ac86955febbd25de7619931bb7dd3c58432181d3b6/68747470733a2f2f626565776172652e6f72672f636f6d6d756e6974792f6d656d626572732f616e61636f6e64612f616e61636f6e64612d6c617267652e706e67" alt="anaconda logo" width="40%"/></a>

README.rst

Lines changed: 0 additions & 18 deletions
This file was deleted.

docs/source/changelog.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
Changelog
22
=========
33

4+
2025.9.0
5+
--------
6+
7+
- update README for distribution compliance
8+
9+
2025.7.0
10+
--------
11+
12+
- fix exclusive write for small files (#974)
13+
- acknowledge Anaconda support (#972)
14+
- fix test typo (#970)
15+
416
2025.5.1
517
--------
618

docs/source/code-of-conduct.rst

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
Code of Conduct
2+
===============
3+
4+
All participants in the fsspec community are expected to adhere to a Code of Conduct.
5+
6+
As contributors and maintainers of this project, and in the interest of
7+
fostering an open and welcoming community, we pledge to respect all people who
8+
contribute through reporting issues, posting feature requests, updating
9+
documentation, submitting pull requests or patches, and other activities.
10+
11+
We are committed to making participation in this project a harassment-free
12+
experience for everyone, treating everyone as unique humans deserving of
13+
respect.
14+
15+
Examples of unacceptable behaviour by participants include:
16+
17+
- The use of sexualized language or imagery
18+
- Personal attacks
19+
- Trolling or insulting/derogatory comments
20+
- Public or private harassment
21+
- Publishing other's private information, such as physical or electronic
22+
addresses, without explicit permission
23+
- Other unethical or unprofessional conduct
24+
25+
Project maintainers have the right and responsibility to remove, edit, or
26+
reject comments, commits, code, wiki edits, issues, and other contributions
27+
that are not aligned to this Code of Conduct, or to ban temporarily or
28+
permanently any contributor for other behaviours that they deem inappropriate,
29+
threatening, offensive, or harmful.
30+
31+
By adopting this Code of Conduct, project maintainers commit themselves
32+
to fairly and consistently applying these principles to every aspect of
33+
managing this project. Project maintainers who do not follow or enforce
34+
the Code of Conduct may be permanently removed from the project team.
35+
36+
This code of conduct applies both within project spaces and in public
37+
spaces when an individual is representing the project or its community.
38+
39+
If you feel the code of conduct has been violated, please report the
40+
incident to the fsspec core team.
41+
42+
Reporting
43+
---------
44+
45+
If you believe someone is violating theCode of Conduct we ask that you report it
46+
to the Project by emailing [email protected]. All reports will be kept
47+
confidential. In some cases we may determine that a public statement will need
48+
to be made. If that's the case, the identities of all victims and reporters
49+
will remain confidential unless those individuals instruct us otherwise.
50+
If you believe anyone is in physical danger, please notify appropriate law
51+
enforcement first.
52+
53+
In your report please include:
54+
55+
- Your contact info
56+
- Names (real, nicknames, or pseudonyms) of any individuals involved.
57+
If there were other witnesses besides you, please try to include them as well.
58+
- When and where the incident occurred. Please be as specific as possible.
59+
- Your account of what occurred. If there is a publicly available record
60+
please include a link.
61+
- Any extra context you believe existed for the incident.
62+
- If you believe this incident is ongoing.
63+
- If you believe any member of the core team has a conflict of interest
64+
in adjudicating the incident.
65+
- What, if any, corrective response you believe would be appropriate.
66+
- Any other information you believe we should have.
67+
68+
Core team members are obligated to maintain confidentiality with regard
69+
to the reporter and details of an incident.
70+
71+
What happens next?
72+
~~~~~~~~~~~~~~~~~~
73+
74+
You will receive an email acknowledging receipt of your complaint.
75+
The core team will immediately meet to review the incident and determine:
76+
77+
- What happened.
78+
- Whether this event constitutes a code of conduct violation.
79+
- Who the bad actor was.
80+
- Whether this is an ongoing situation, or if there is a threat to anyone's
81+
physical safety.
82+
- If this is determined to be an ongoing incident or a threat to physical safety,
83+
the working groups' immediate priority will be to protect everyone involved.
84+
85+
If a member of the core team is one of the named parties, they will not be
86+
included in any discussions, and will not be provided with any confidential
87+
details from the reporter.
88+
89+
If anyone on the core team believes they have a conflict of interest in
90+
adjudicating on a reported issue, they will inform the other core team
91+
members, and exempt themselves from any discussion about the issue.
92+
Following this declaration, they will not be provided with any confidential
93+
details from the reporter.
94+
95+
Once the working group has a complete account of the events they will make a
96+
decision as to how to response. Responses may include:
97+
98+
- Nothing (if we determine no violation occurred).
99+
- A private reprimand from the working group to the individual(s) involved.
100+
- A public reprimand.
101+
- An imposed vacation
102+
- A permanent or temporary ban from some or all spaces (GitHub repositories, etc.)
103+
- A request for a public or private apology.
104+
105+
We'll respond within one week to the person who filed the report with either a
106+
resolution or an explanation of why the situation is not yet resolved.
107+
108+
Once we've determined our final action, we'll contact the original reporter
109+
to let them know what action (if any) we'll be taking. We'll take into account
110+
feedback from the reporter on the appropriateness of our response, but we
111+
don't guarantee we'll act on it.
112+
113+
Acknowledgement
114+
---------------
115+
116+
This CoC is modified from the one by `BeeWare`_, which in turn refers to
117+
the `Contributor Covenant`_ and the `Django`_ project.
118+
119+
.. _BeeWare: https://beeware.org/community/behavior/code-of-conduct/
120+
.. _Contributor Covenant: https://www.contributor-covenant.org/version/1/3/0/code-of-conduct/
121+
.. _Django: https://www.djangoproject.com/conduct/reporting/
122+
123+
.. raw:: html
124+
125+
<script data-goatcounter="https://projspec.goatcounter.com/count"
126+
async src="//gc.zgo.at/count.js"></script>

docs/source/index.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ Contents
340340
development
341341
api
342342
changelog
343+
code-of-conduct
343344
:maxdepth: 2
344345

345346

@@ -351,3 +352,12 @@ Indices and tables
351352
* :ref:`genindex`
352353
* :ref:`modindex`
353354
* :ref:`search`
355+
356+
357+
These docs pages collect anonymous tracking data using goatcounter, and the
358+
dashboard is available to the public: https://s3fs.goatcounter.com/ .
359+
360+
.. raw:: html
361+
362+
<script data-goatcounter="https://s3fs.goatcounter.com/count"
363+
async src="//gc.zgo.at/count.js"></script>

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
aiobotocore>=2.5.4,<3.0.0
2-
fsspec==2025.5.1
2+
fsspec==2025.9.0
33
aiohttp!=4.0.0a0, !=4.0.0a1

s3fs/core.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,18 @@ async def _info(self, path, bucket=None, key=None, refresh=False, version_id=Non
14781478
pass
14791479
except ClientError as e:
14801480
raise translate_boto_error(e, set_cause=False)
1481+
else:
1482+
try:
1483+
out = await self._call_s3("head_bucket", Bucket=bucket, **self.req_kw)
1484+
return {
1485+
"name": bucket,
1486+
"type": "directory",
1487+
"size": 0,
1488+
"StorageClass": "DIRECTORY",
1489+
"VersionId": out.get("VersionId"),
1490+
}
1491+
except ClientError as e:
1492+
raise translate_boto_error(e, set_cause=False)
14811493

14821494
try:
14831495
# We check to see if the path is a directory by attempting to list its
@@ -2472,6 +2484,7 @@ def n_bytes_left() -> int:
24722484

24732485
def commit(self):
24742486
logger.debug("Commit %s" % self)
2487+
match = {"IfNoneMatch": "*"} if "x" in self.mode else {}
24752488
if self.tell() == 0:
24762489
if self.buffer is not None:
24772490
logger.debug("Empty file committed %s" % self)
@@ -2485,15 +2498,11 @@ def commit(self):
24852498
kw = dict(Key=self.key, Bucket=self.bucket, Body=data, **self.kwargs)
24862499
if self.acl:
24872500
kw["ACL"] = self.acl
2488-
write_result = self._call_s3("put_object", **kw)
2501+
write_result = self._call_s3("put_object", **kw, **match)
24892502
else:
24902503
raise RuntimeError
24912504
else:
24922505
logger.debug("Complete multi-part upload for %s " % self)
2493-
if "x" in self.mode:
2494-
match = {"IfNoneMatch": "*"}
2495-
else:
2496-
match = {}
24972506
part_info = {"Parts": self.parts}
24982507
write_result = self._call_s3(
24992508
"complete_multipart_upload",

s3fs/tests/derived/s3fs_test.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ class TestS3fsPipe(abstract.AbstractPipeTests, S3fsFixtures):
3333

3434

3535
class TestS3fsOpen(abstract.AbstractOpenTests, S3fsFixtures):
36-
3736
test_open_exclusive = pytest.mark.xfail(
3837
reason="complete_multipart_upload doesn't implement condition in moto"
3938
)(abstract.AbstractOpenTests.test_open_exclusive)

s3fs/tests/test_s3fs.py

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import fsspec.core
1919
from dateutil.tz import tzutc
2020

21+
import botocore
2122
import s3fs.core
2223
from s3fs.core import S3FileSystem
2324
from s3fs.utils import ignoring, SSEParams
@@ -2888,7 +2889,14 @@ def test_exist_after_delete(s3):
28882889
assert not s3.exists(test_dir)
28892890

28902891

2891-
@pytest.mark.xfail(reason="moto doesn't support conditional MPU")
2892+
# condition: True if running on botocore < 1.36.0
2893+
# The below tests for exclusive writes will fail on older versions of botocore.
2894+
old_botocore = version.parse(botocore.__version__) < version.parse("1.36.0")
2895+
2896+
2897+
@pytest.mark.xfail(
2898+
reason="moto doesn't support IfNoneMatch for MPU when object created via MPU"
2899+
)
28922900
def test_pipe_exclusive_big(s3):
28932901
chunksize = 5 * 2**20 # minimum allowed
28942902
data = b"x" * chunksize * 3
@@ -2899,17 +2907,89 @@ def test_pipe_exclusive_big(s3):
28992907
assert not s3.list_multipart_uploads(test_bucket_name)
29002908

29012909

2902-
@pytest.mark.xfail(reason="moto doesn't support conditional MPU")
2903-
def test_put_exclusive_big(s3, tempdir):
2910+
@pytest.mark.xfail(
2911+
old_botocore, reason="botocore<1.33.0 lacks IfNoneMatch support", strict=True
2912+
)
2913+
def test_pipe_exclusive_big_after_small(s3):
2914+
"""Test conditional MPU after creating object via put_object
2915+
2916+
This test is required because moto's implementation of IfNoneMatch for MPU
2917+
only works when the object is initially created via put_object and not via
2918+
MPU.
2919+
"""
29042920
chunksize = 5 * 2**20 # minimum allowed
2905-
data = b"x" * chunksize * 3
2906-
fn = f"{tempdir}/afile"
2907-
with open(fn, "wb") as f:
2908-
f.write(fn)
2909-
s3.put(fn, f"{test_bucket_name}/afile", data, mode="overwrite", chunksize=chunksize)
2910-
s3.put(fn, f"{test_bucket_name}/afile", data, mode="overwrite", chunksize=chunksize)
2921+
2922+
# First, create object via put_object (small upload)
2923+
s3.pipe(f"{test_bucket_name}/afile", b"small", mode="overwrite")
2924+
2925+
# Now try multipart upload with mode="create" (should fail)
29112926
with pytest.raises(FileExistsError):
2912-
s3.put(
2913-
fn, f"{test_bucket_name}/afile", data, mode="create", chunksize=chunksize
2927+
s3.pipe(
2928+
f"{test_bucket_name}/afile",
2929+
b"c" * chunksize * 3,
2930+
mode="create",
2931+
chunksize=chunksize,
29142932
)
2933+
29152934
assert not s3.list_multipart_uploads(test_bucket_name)
2935+
2936+
2937+
@pytest.mark.xfail(
2938+
reason="moto doesn't support IfNoneMatch for MPU when object created via MPU"
2939+
)
2940+
def test_put_exclusive_big(s3, tmpdir):
2941+
chunksize = 5 * 2**20 # minimum allowed
2942+
fn = f"{tmpdir}/afile"
2943+
with open(fn, "wb") as f:
2944+
f.write(b"x" * chunksize * 3)
2945+
s3.put(fn, f"{test_bucket_name}/afile", mode="overwrite", chunksize=chunksize)
2946+
s3.put(fn, f"{test_bucket_name}/afile", mode="overwrite", chunksize=chunksize)
2947+
with pytest.raises(FileExistsError):
2948+
s3.put(fn, f"{test_bucket_name}/afile", mode="create", chunksize=chunksize)
2949+
assert not s3.list_multipart_uploads(test_bucket_name)
2950+
2951+
2952+
@pytest.mark.xfail(
2953+
old_botocore, reason="botocore<1.33.0 lacks IfNoneMatch support", strict=True
2954+
)
2955+
def test_put_exclusive_big_after_small(s3, tmpdir):
2956+
"""Test conditional MPU after creating object via put_object.
2957+
2958+
This test is required because moto's implementation of IfNoneMatch for MPU
2959+
only works when the object is initially created via put_object and not via
2960+
MPU.
2961+
"""
2962+
chunksize = 5 * 2**20 # minimum allowed
2963+
fn = str(tmpdir.join("afile"))
2964+
with open(fn, "wb") as f:
2965+
f.write(b"x" * chunksize * 3)
2966+
2967+
# First, create object via put_object (small upload)
2968+
s3.pipe(f"{test_bucket_name}/afile", b"small", mode="overwrite")
2969+
2970+
# Now try multipart upload with mode="create" (should fail)
2971+
with pytest.raises(FileExistsError):
2972+
s3.put(fn, f"{test_bucket_name}/afile", mode="create", chunksize=chunksize)
2973+
2974+
assert not s3.list_multipart_uploads(test_bucket_name)
2975+
2976+
2977+
@pytest.mark.xfail(
2978+
old_botocore, reason="botocore<1.33.0 lacks IfNoneMatch support", strict=True
2979+
)
2980+
def test_put_exclusive_small(s3, tmpdir):
2981+
fn = f"{tmpdir}/afile"
2982+
with open(fn, "wb") as f:
2983+
f.write(b"x")
2984+
s3.put(fn, f"{test_bucket_name}/afile", mode="overwrite")
2985+
s3.put(fn, f"{test_bucket_name}/afile", mode="overwrite")
2986+
with pytest.raises(FileExistsError):
2987+
s3.put(fn, f"{test_bucket_name}/afile", mode="create")
2988+
assert not s3.list_multipart_uploads(test_bucket_name)
2989+
2990+
2991+
def test_bucket_info(s3):
2992+
info = s3.info(test_bucket_name)
2993+
assert "VersionId" in info
2994+
assert info["type"] == "directory"
2995+
assert info["name"] == test_bucket_name

0 commit comments

Comments
 (0)